@blinkdotnew/cli 0.2.3 → 0.2.5

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
@@ -268,6 +268,7 @@ Commands:
268
268
  animate Image-to-video animation (URL or local file)
269
269
  speech Text-to-speech (saves .mp3)
270
270
  transcribe Audio-to-text (file or URL)
271
+ call Make an AI phone call to any US/International number
271
272
 
272
273
  Examples:
273
274
  $ blink ai text "Write a product description for a CLI tool"
@@ -281,6 +282,7 @@ Examples:
281
282
  $ blink ai speech "Hello, welcome to Blink." --voice nova --output hello.mp3
282
283
  $ blink ai transcribe ./meeting.mp3 --language en
283
284
  $ blink ai transcribe https://example.com/audio.mp3
285
+ $ blink ai call "+14155551234" "Collect payment from John Smith"
284
286
 
285
287
  No project needed \u2014 AI commands are workspace-scoped.
286
288
  Add --json to any command for machine-readable output.
@@ -476,6 +478,92 @@ Supported formats: mp3, wav, m4a, ogg
476
478
  if (isJsonMode()) return printJson(result);
477
479
  console.log(result?.text ?? result?.transcript ?? JSON.stringify(result));
478
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", `
482
+ Examples:
483
+ $ blink ai call "+14155551234" "You are collecting a payment of $240 from John Smith. Be polite but firm."
484
+ $ blink ai call "+14155551234" "Confirm John's appointment for tomorrow at 3pm" --voice openai:nova
485
+ $ blink ai call "+14155551234" "Leave a message about our new product launch" --no-wait
486
+ $ blink ai call "+14155551234" "Your task" --json | jq '.call_id'
487
+
488
+ Phone number must be in E.164 format: +1XXXXXXXXXX (US), +44XXXXXXXXXX (UK), etc.
489
+
490
+ Voices:
491
+ openai:alloy Balanced, neutral (default)
492
+ openai:nova Friendly, warm
493
+ openai:echo Clear, conversational
494
+ openai:onyx Deep, authoritative
495
+ openai:shimmer Soft, gentle
496
+ cartesia:sonic-english Low-latency, natural
497
+
498
+ Call is charged to your workspace credits after completion (~1 credit/min).
499
+ `).action(async (phoneNumber, systemPrompt, opts) => {
500
+ requireToken();
501
+ const result = await withSpinner(
502
+ "Initiating AI call...",
503
+ () => resourcesRequest("/api/v1/ai/call", {
504
+ body: {
505
+ phone_number: phoneNumber,
506
+ system_prompt: systemPrompt,
507
+ voice: opts.voice,
508
+ max_duration_seconds: parseInt(opts.maxDuration)
509
+ }
510
+ })
511
+ );
512
+ if (isJsonMode()) return printJson(result);
513
+ const callId = result?.call_id;
514
+ if (opts.noWait) {
515
+ console.log(`Call initiated: ${callId}`);
516
+ console.log(`Poll status: blink ai call-status ${callId}`);
517
+ return;
518
+ }
519
+ console.log(`Call started \u2192 ${callId}`);
520
+ console.log(`Calling ${phoneNumber}...`);
521
+ let status = "queued";
522
+ let attempts = 0;
523
+ while (!["completed", "failed", "no-answer", "busy"].includes(status) && attempts < 120) {
524
+ await new Promise((r) => setTimeout(r, 5e3));
525
+ attempts++;
526
+ const poll = await resourcesRequest(`/api/v1/ai/call/${callId}`, { method: "GET" }).catch(() => null);
527
+ if (poll) {
528
+ status = poll.status;
529
+ if (["completed", "failed", "no-answer", "busy"].includes(status)) {
530
+ if (status === "completed") {
531
+ console.log(`
532
+ Call completed (${poll.duration_seconds}s)`);
533
+ if (poll.transcript) console.log(`
534
+ Transcript:
535
+ ${poll.transcript}`);
536
+ if (poll.credits_charged) console.log(`
537
+ Credits charged: ${poll.credits_charged}`);
538
+ } else {
539
+ console.log(`
540
+ Call ended: ${status}`);
541
+ }
542
+ return;
543
+ }
544
+ }
545
+ }
546
+ console.log(`
547
+ Call in progress. Check status: blink ai call-status ${callId}`);
548
+ });
549
+ ai.command("call-status <call-id>").description("Get status of an AI phone call").addHelpText("after", `
550
+ Examples:
551
+ $ blink ai call-status vc_a1b2c3d4
552
+ $ blink ai call-status vc_a1b2c3d4 --json
553
+ `).action(async (callId, _opts) => {
554
+ requireToken();
555
+ const result = await withSpinner(
556
+ "Fetching call status...",
557
+ () => resourcesRequest(`/api/v1/ai/call/${callId}`, { method: "GET" })
558
+ );
559
+ if (isJsonMode()) return printJson(result);
560
+ console.log(`Status: ${result?.status}`);
561
+ if (result?.duration_seconds) console.log(`Duration: ${result.duration_seconds}s`);
562
+ if (result?.transcript) console.log(`
563
+ Transcript:
564
+ ${result.transcript}`);
565
+ if (result?.credits_charged) console.log(`Credits: ${result.credits_charged}`);
566
+ });
479
567
  }
480
568
 
481
569
  // src/commands/web.ts
@@ -1067,7 +1155,7 @@ Once linked, use \`blink connector exec\` to call their APIs without managing to
1067
1155
 
1068
1156
  Connect accounts at: https://blink.new/settings?tab=connectors
1069
1157
 
1070
- Run \`blink connector providers\` to see all 39 supported providers.
1158
+ Run \`blink connector providers\` to see all 38 supported providers.
1071
1159
 
1072
1160
  Quick examples:
1073
1161
  $ blink connector exec github /user/repos GET
@@ -1198,6 +1286,231 @@ Provider base URLs used:
1198
1286
  });
1199
1287
  }
1200
1288
 
1289
+ // src/commands/linkedin.ts
1290
+ init_agent();
1291
+ import chalk7 from "chalk";
1292
+ var NOT_LINKED = "LinkedIn not linked. Link it in the Agent Integrations tab at blink.new/claw";
1293
+ async function liExec(method, httpMethod, params, agentId) {
1294
+ const result = await resourcesRequest("/v1/connectors/linkedin/execute", {
1295
+ body: { method, http_method: httpMethod, params },
1296
+ headers: { "x-blink-agent-id": agentId }
1297
+ });
1298
+ if (!result?.success) throw new Error(result?.error ?? NOT_LINKED);
1299
+ return result.data;
1300
+ }
1301
+ async function getPersonId(agentId) {
1302
+ const data = await liExec("v2/userinfo", "GET", {}, agentId);
1303
+ const id = data?.sub ?? data?.id;
1304
+ if (!id) throw new Error("Could not resolve LinkedIn person ID");
1305
+ return id;
1306
+ }
1307
+ function registerLinkedInCommands(program2) {
1308
+ const li = program2.command("linkedin").description("LinkedIn connector \u2014 post content, manage comments, and view your profile").addHelpText("after", `
1309
+ LinkedIn must be linked to your agent via the Integrations tab at blink.new/claw.
1310
+ Agent ID defaults to BLINK_AGENT_ID (automatically set on Claw Fly machines).
1311
+
1312
+ Examples:
1313
+ $ blink linkedin me Show your LinkedIn profile
1314
+ $ blink linkedin posts List your 10 most recent posts
1315
+ $ blink linkedin post "Excited to announce..." Publish a text post
1316
+ $ blink linkedin comments "urn:li:ugcPost:123" Read comments on a post
1317
+ $ blink linkedin comment "urn:li:ugcPost:123" "Great post!" Add a comment
1318
+ $ blink linkedin like "urn:li:ugcPost:123" Like a post
1319
+ $ blink linkedin unlike "urn:li:ugcPost:123" Unlike a post
1320
+ `);
1321
+ li.command("me").description("Show your LinkedIn profile (name, ID, vanity URL)").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1322
+ Examples:
1323
+ $ blink linkedin me
1324
+ $ blink linkedin me --json
1325
+ $ blink linkedin me --agent clw_xxx
1326
+ `).action(async (opts) => {
1327
+ requireToken();
1328
+ const agentId = requireAgentId(opts.agent);
1329
+ const data = await withSpinner(
1330
+ "Fetching LinkedIn profile...",
1331
+ () => liExec("v2/userinfo", "GET", {}, agentId)
1332
+ );
1333
+ if (isJsonMode()) return printJson(data);
1334
+ const name = data?.name ?? [data?.given_name, data?.family_name].filter(Boolean).join(" ");
1335
+ const personId = data?.sub ?? data?.id;
1336
+ console.log(chalk7.bold("LinkedIn Profile"));
1337
+ if (personId) console.log(` ${chalk7.dim("ID:")} ${personId}`);
1338
+ if (name) console.log(` ${chalk7.dim("Name:")} ${name}`);
1339
+ if (data?.email) console.log(` ${chalk7.dim("Email:")} ${data.email}`);
1340
+ });
1341
+ li.command("posts").description("List your most recent LinkedIn posts (last 10)").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").option("--limit <n>", "Number of posts to fetch (default: 10)", "10").addHelpText("after", `
1342
+ Examples:
1343
+ $ blink linkedin posts
1344
+ $ blink linkedin posts --limit 5
1345
+ $ blink linkedin posts --json | jq '.[].id'
1346
+ `).action(async (opts) => {
1347
+ requireToken();
1348
+ const agentId = requireAgentId(opts.agent);
1349
+ const personId = await withSpinner(
1350
+ "Resolving your LinkedIn identity...",
1351
+ () => getPersonId(agentId)
1352
+ );
1353
+ const urn = encodeURIComponent(`urn:li:person:${personId}`);
1354
+ const data = await withSpinner(
1355
+ "Fetching posts...",
1356
+ () => liExec(
1357
+ `ugcPosts?q=authors&authors=List(${urn})&sortBy=LAST_MODIFIED&count=${opts.limit}`,
1358
+ "GET",
1359
+ {},
1360
+ agentId
1361
+ )
1362
+ );
1363
+ const posts = data?.elements ?? (Array.isArray(data) ? data : []);
1364
+ if (isJsonMode()) return printJson(posts);
1365
+ if (!posts.length) {
1366
+ console.log(chalk7.dim("No posts found."));
1367
+ return;
1368
+ }
1369
+ for (const post of posts) {
1370
+ const id = post.id ?? "\u2014";
1371
+ const content = post.specificContent;
1372
+ const share = content?.["com.linkedin.ugc.ShareContent"];
1373
+ const commentary = share?.shareCommentary;
1374
+ const text = commentary?.text ?? post.text?.text ?? "(no text)";
1375
+ const preview = text.length > 120 ? text.slice(0, 120) + "\u2026" : text;
1376
+ console.log(chalk7.bold(id));
1377
+ console.log(` ${chalk7.dim(preview)}`);
1378
+ console.log();
1379
+ }
1380
+ console.log(chalk7.dim(`${posts.length} post(s)`));
1381
+ });
1382
+ li.command("post <text>").description("Publish a text post to LinkedIn").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").option("--visibility <vis>", "Post visibility: PUBLIC | CONNECTIONS (default: PUBLIC)", "PUBLIC").addHelpText("after", `
1383
+ Examples:
1384
+ $ blink linkedin post "Excited to share our latest update!"
1385
+ $ blink linkedin post "Internal update" --visibility CONNECTIONS
1386
+ $ blink linkedin post "Hello LinkedIn" --json
1387
+ `).action(async (text, opts) => {
1388
+ requireToken();
1389
+ const agentId = requireAgentId(opts.agent);
1390
+ const data = await withSpinner(
1391
+ "Publishing post...",
1392
+ () => liExec("/ugcPosts", "POST", { text, visibility: opts.visibility }, agentId)
1393
+ );
1394
+ if (isJsonMode()) return printJson(data);
1395
+ console.log(chalk7.green("\u2713 Post published"));
1396
+ if (data?.id) console.log(chalk7.dim(` URN: ${data.id}`));
1397
+ });
1398
+ li.command("comments <postUrn>").description("Read comments on a LinkedIn post").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1399
+ <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
1400
+ Use "blink linkedin posts" to find your post URNs.
1401
+
1402
+ Examples:
1403
+ $ blink linkedin comments "urn:li:ugcPost:1234567890"
1404
+ $ blink linkedin comments "urn:li:ugcPost:1234567890" --json
1405
+ `).action(async (postUrn, opts) => {
1406
+ requireToken();
1407
+ const agentId = requireAgentId(opts.agent);
1408
+ const encoded = encodeURIComponent(postUrn);
1409
+ const data = await withSpinner(
1410
+ "Fetching comments...",
1411
+ () => liExec(`rest/socialActions/${encoded}/comments`, "GET", {}, agentId)
1412
+ );
1413
+ const comments = data?.elements ?? (Array.isArray(data) ? data : []);
1414
+ if (isJsonMode()) return printJson(comments);
1415
+ if (!comments.length) {
1416
+ console.log(chalk7.dim("No comments."));
1417
+ return;
1418
+ }
1419
+ for (const c of comments) {
1420
+ const author = c.actor ?? "\u2014";
1421
+ const msg = c.message;
1422
+ const text = msg?.text ?? "(no text)";
1423
+ console.log(chalk7.bold(author));
1424
+ console.log(` ${text}`);
1425
+ console.log();
1426
+ }
1427
+ console.log(chalk7.dim(`${comments.length} comment(s)`));
1428
+ });
1429
+ 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", `
1430
+ <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
1431
+
1432
+ Examples:
1433
+ $ blink linkedin comment "urn:li:ugcPost:1234567890" "Great post!"
1434
+ $ blink linkedin comment "urn:li:ugcPost:1234567890" "Thanks for sharing" --json
1435
+ `).action(async (postUrn, text, opts) => {
1436
+ requireToken();
1437
+ const agentId = requireAgentId(opts.agent);
1438
+ const personId = await withSpinner(
1439
+ "Resolving your LinkedIn identity...",
1440
+ () => getPersonId(agentId)
1441
+ );
1442
+ const actor = `urn:li:person:${personId}`;
1443
+ const encoded = encodeURIComponent(postUrn);
1444
+ const data = await withSpinner(
1445
+ "Adding comment...",
1446
+ () => liExec(
1447
+ `rest/socialActions/${encoded}/comments`,
1448
+ "POST",
1449
+ { actor, object: postUrn, message: { text } },
1450
+ agentId
1451
+ )
1452
+ );
1453
+ if (isJsonMode()) return printJson(data);
1454
+ console.log(chalk7.green("\u2713 Comment added"));
1455
+ if (data?.id) console.log(chalk7.dim(` ID: ${data.id}`));
1456
+ });
1457
+ li.command("like <postUrn>").description("Like a LinkedIn post").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1458
+ <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
1459
+
1460
+ Examples:
1461
+ $ blink linkedin like "urn:li:ugcPost:1234567890"
1462
+ $ blink linkedin like "urn:li:ugcPost:1234567890" --json
1463
+ `).action(async (postUrn, opts) => {
1464
+ requireToken();
1465
+ const agentId = requireAgentId(opts.agent);
1466
+ const personId = await withSpinner(
1467
+ "Resolving your LinkedIn identity...",
1468
+ () => getPersonId(agentId)
1469
+ );
1470
+ const actor = `urn:li:person:${personId}`;
1471
+ const encoded = encodeURIComponent(postUrn);
1472
+ const data = await withSpinner(
1473
+ "Liking post...",
1474
+ () => liExec(
1475
+ `rest/socialActions/${encoded}/likes`,
1476
+ "POST",
1477
+ { actor, object: postUrn },
1478
+ agentId
1479
+ )
1480
+ );
1481
+ if (isJsonMode()) return printJson(data);
1482
+ console.log(chalk7.green("\u2713 Post liked"));
1483
+ });
1484
+ li.command("unlike <postUrn>").description("Unlike a LinkedIn post").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1485
+ <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
1486
+
1487
+ Examples:
1488
+ $ blink linkedin unlike "urn:li:ugcPost:1234567890"
1489
+ $ blink linkedin unlike "urn:li:ugcPost:1234567890" --json
1490
+ `).action(async (postUrn, opts) => {
1491
+ requireToken();
1492
+ const agentId = requireAgentId(opts.agent);
1493
+ const personId = await withSpinner(
1494
+ "Resolving your LinkedIn identity...",
1495
+ () => getPersonId(agentId)
1496
+ );
1497
+ const actor = `urn:li:person:${personId}`;
1498
+ const encodedPost = encodeURIComponent(postUrn);
1499
+ const encodedActor = encodeURIComponent(actor);
1500
+ const data = await withSpinner(
1501
+ "Unliking post...",
1502
+ () => liExec(
1503
+ `rest/socialActions/${encodedPost}/likes/${encodedActor}?actor=${encodedActor}`,
1504
+ "DELETE",
1505
+ {},
1506
+ agentId
1507
+ )
1508
+ );
1509
+ if (isJsonMode()) return printJson(data);
1510
+ console.log(chalk7.green("\u2713 Post unliked"));
1511
+ });
1512
+ }
1513
+
1201
1514
  // src/lib/api-app.ts
1202
1515
  var BASE_URL2 = process.env.BLINK_APP_URL ?? "https://blink.new";
1203
1516
  async function appRequest(path, opts = {}) {
@@ -1236,7 +1549,7 @@ async function appRequest(path, opts = {}) {
1236
1549
  init_project();
1237
1550
  import { readdirSync, readFileSync as readFileSync8 } from "fs";
1238
1551
  import { join as join3, relative } from "path";
1239
- import chalk7 from "chalk";
1552
+ import chalk8 from "chalk";
1240
1553
  function collectFiles(dir) {
1241
1554
  const files = [];
1242
1555
  function walk(current) {
@@ -1309,7 +1622,7 @@ Project resolution:
1309
1622
  const production = opts.prod === true;
1310
1623
  if (!isJsonMode()) {
1311
1624
  console.log();
1312
- console.log(chalk7.bold(" Blink Deploy"));
1625
+ console.log(chalk8.bold(" Blink Deploy"));
1313
1626
  console.log();
1314
1627
  }
1315
1628
  const files = await withSpinner(`Packaging ${buildDir}...`, async () => collectFiles(buildDir));
@@ -1363,7 +1676,7 @@ Project resolution:
1363
1676
 
1364
1677
  // src/commands/project.ts
1365
1678
  init_project();
1366
- import chalk8 from "chalk";
1679
+ import chalk9 from "chalk";
1367
1680
  function registerProjectCommands(program2) {
1368
1681
  const project = program2.command("project").description("Create, list, and delete Blink projects").addHelpText("after", `
1369
1682
  Examples:
@@ -1392,8 +1705,8 @@ After creating a project, link it to your current directory:
1392
1705
  );
1393
1706
  if (isJsonMode()) return printJson(result);
1394
1707
  const proj = result?.project ?? result;
1395
- console.log(chalk8.green("\u2713") + ` Created: ${proj.id}`);
1396
- console.log(chalk8.dim(" Run `blink link " + proj.id + "` to use it"));
1708
+ console.log(chalk9.green("\u2713") + ` Created: ${proj.id}`);
1709
+ console.log(chalk9.dim(" Run `blink link " + proj.id + "` to use it"));
1397
1710
  });
1398
1711
  project.command("delete <project_id>").description("Delete a project").option("--yes", "Skip confirmation").action(async (projectId, opts) => {
1399
1712
  requireToken();
@@ -1436,7 +1749,7 @@ After linking, most commands work without specifying a project_id:
1436
1749
  });
1437
1750
  }
1438
1751
  writeProjectConfig({ projectId: id });
1439
- console.log(chalk8.green("\u2713") + " Linked to " + id);
1752
+ console.log(chalk9.green("\u2713") + " Linked to " + id);
1440
1753
  });
1441
1754
  program2.command("unlink").description("Remove project link from current directory").action(() => {
1442
1755
  clearProjectConfig();
@@ -1448,29 +1761,29 @@ After linking, most commands work without specifying a project_id:
1448
1761
  const agentId = resolveAgentId3();
1449
1762
  const agentSource = process.env.BLINK_AGENT_ID ? "BLINK_AGENT_ID env" : process.env.BLINK_ACTIVE_AGENT ? "BLINK_ACTIVE_AGENT env" : null;
1450
1763
  if (agentId) {
1451
- console.log(chalk8.bold("Agent ") + agentId + chalk8.dim(" (" + agentSource + ")"));
1764
+ console.log(chalk9.bold("Agent ") + agentId + chalk9.dim(" (" + agentSource + ")"));
1452
1765
  } else {
1453
- console.log(chalk8.dim("Agent not set (use: eval $(blink agent use clw_xxx --export))"));
1766
+ console.log(chalk9.dim("Agent not set (use: eval $(blink agent use clw_xxx --export))"));
1454
1767
  }
1455
1768
  if (config) {
1456
1769
  const projectSource = process.env.BLINK_ACTIVE_PROJECT ? "BLINK_ACTIVE_PROJECT env" : ".blink/project.json";
1457
- console.log(chalk8.bold("Project ") + config.projectId + chalk8.dim(" (" + projectSource + ")"));
1770
+ console.log(chalk9.bold("Project ") + config.projectId + chalk9.dim(" (" + projectSource + ")"));
1458
1771
  } else if (process.env.BLINK_ACTIVE_PROJECT) {
1459
- console.log(chalk8.bold("Project ") + process.env.BLINK_ACTIVE_PROJECT + chalk8.dim(" (BLINK_ACTIVE_PROJECT env)"));
1772
+ console.log(chalk9.bold("Project ") + process.env.BLINK_ACTIVE_PROJECT + chalk9.dim(" (BLINK_ACTIVE_PROJECT env)"));
1460
1773
  } else {
1461
- console.log(chalk8.dim("Project not linked (use: blink link or eval $(blink use proj_xxx --export))"));
1774
+ console.log(chalk9.dim("Project not linked (use: blink link or eval $(blink use proj_xxx --export))"));
1462
1775
  }
1463
1776
  const authSource = process.env.BLINK_API_KEY ? "BLINK_API_KEY env" : "~/.config/blink/config.toml";
1464
1777
  const hasProjectKey = !!process.env.BLINK_PROJECT_KEY;
1465
- console.log(chalk8.bold("Auth ") + authSource);
1778
+ console.log(chalk9.bold("Auth ") + authSource);
1466
1779
  if (hasProjectKey) {
1467
- console.log(chalk8.bold("ProjKey ") + "BLINK_PROJECT_KEY env" + chalk8.dim(" (used for db/storage/rag)"));
1780
+ console.log(chalk9.bold("ProjKey ") + "BLINK_PROJECT_KEY env" + chalk9.dim(" (used for db/storage/rag)"));
1468
1781
  }
1469
1782
  });
1470
1783
  }
1471
1784
 
1472
1785
  // src/commands/auth.ts
1473
- import chalk9 from "chalk";
1786
+ import chalk10 from "chalk";
1474
1787
  function registerAuthCommands(program2) {
1475
1788
  program2.command("login").description("Authenticate with your Blink API key").option("--interactive", "Prompt for API key (for headless/SSH/CI environments)").addHelpText("after", `
1476
1789
  Get your API key at: blink.new \u2192 Settings \u2192 API Keys (starts with blnk_ak_)
@@ -1483,7 +1796,7 @@ In Blink Claw agents: BLINK_API_KEY is already set \u2014 login is not needed.
1483
1796
  For CI/GitHub Actions: set BLINK_API_KEY as a secret, skip login entirely.
1484
1797
  `).action(async (opts) => {
1485
1798
  if (process.env.BLINK_API_KEY && !opts.interactive) {
1486
- console.log(chalk9.green("\u2713") + " Already authenticated via BLINK_API_KEY env var.");
1799
+ console.log(chalk10.green("\u2713") + " Already authenticated via BLINK_API_KEY env var.");
1487
1800
  return;
1488
1801
  }
1489
1802
  const { password } = await import("@clack/prompts");
@@ -1493,7 +1806,7 @@ For CI/GitHub Actions: set BLINK_API_KEY as a secret, skip login entirely.
1493
1806
  process.exit(1);
1494
1807
  }
1495
1808
  writeConfig({ api_key: apiKey });
1496
- console.log(chalk9.green("\u2713") + " Saved to ~/.config/blink/config.toml");
1809
+ console.log(chalk10.green("\u2713") + " Saved to ~/.config/blink/config.toml");
1497
1810
  });
1498
1811
  program2.command("logout").description("Remove stored credentials").action(() => {
1499
1812
  clearConfig();
@@ -1513,15 +1826,15 @@ For CI/GitHub Actions: set BLINK_API_KEY as a secret, skip login entirely.
1513
1826
  });
1514
1827
  }
1515
1828
  const source = process.env.BLINK_API_KEY === token ? "BLINK_API_KEY env" : "~/.config/blink/config.toml";
1516
- console.log(chalk9.green("\u2713") + " Authenticated");
1517
- console.log(chalk9.bold("Key ") + token.slice(0, 20) + chalk9.dim("..."));
1518
- console.log(chalk9.bold("Source ") + chalk9.dim(source));
1829
+ console.log(chalk10.green("\u2713") + " Authenticated");
1830
+ console.log(chalk10.bold("Key ") + token.slice(0, 20) + chalk10.dim("..."));
1831
+ console.log(chalk10.bold("Source ") + chalk10.dim(source));
1519
1832
  });
1520
1833
  }
1521
1834
 
1522
1835
  // src/commands/agent.ts
1523
1836
  init_agent();
1524
- import chalk10 from "chalk";
1837
+ import chalk11 from "chalk";
1525
1838
  function registerAgentCommands(program2) {
1526
1839
  const agent = program2.command("agent").description("Manage Blink Claw agents in your workspace").addHelpText("after", `
1527
1840
  Examples:
@@ -1541,7 +1854,7 @@ Agent ID resolution for all agent/secrets commands (priority: high \u2192 low):
1541
1854
  if (isJsonMode()) return printJson(result);
1542
1855
  const agents = Array.isArray(result) ? result : result?.agents ?? [];
1543
1856
  if (!agents.length) {
1544
- console.log(chalk10.dim("No agents found. Deploy one at blink.new/claw"));
1857
+ console.log(chalk11.dim("No agents found. Deploy one at blink.new/claw"));
1545
1858
  return;
1546
1859
  }
1547
1860
  const table = createTable(["ID", "Name", "Status", "Size", "Model"]);
@@ -1564,9 +1877,9 @@ After setting, secrets commands use this agent automatically:
1564
1877
  process.stdout.write(`export BLINK_ACTIVE_AGENT=${agentId}
1565
1878
  `);
1566
1879
  } else {
1567
- console.log(chalk10.bold("Active agent: ") + agentId);
1568
- console.log(chalk10.dim(`Run: export BLINK_ACTIVE_AGENT=${agentId}`));
1569
- console.log(chalk10.dim(`Or: eval $(blink agent use ${agentId} --export)`));
1880
+ console.log(chalk11.bold("Active agent: ") + agentId);
1881
+ console.log(chalk11.dim(`Run: export BLINK_ACTIVE_AGENT=${agentId}`));
1882
+ console.log(chalk11.dim(`Or: eval $(blink agent use ${agentId} --export)`));
1570
1883
  }
1571
1884
  });
1572
1885
  agent.command("status [agent_id]").description("Show details for an agent").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID or BLINK_ACTIVE_AGENT)").addHelpText("after", `
@@ -1582,18 +1895,18 @@ Examples:
1582
1895
  );
1583
1896
  if (isJsonMode()) return printJson(result);
1584
1897
  const a = result?.agent ?? result;
1585
- console.log(chalk10.bold("ID ") + a.id);
1586
- console.log(chalk10.bold("Name ") + a.name);
1587
- console.log(chalk10.bold("Status ") + a.status);
1588
- console.log(chalk10.bold("Model ") + (a.model ?? "-"));
1589
- console.log(chalk10.bold("Machine ") + (a.machine_size ?? "-"));
1590
- if (a.fly_app_name) console.log(chalk10.bold("Fly App ") + a.fly_app_name);
1898
+ console.log(chalk11.bold("ID ") + a.id);
1899
+ console.log(chalk11.bold("Name ") + a.name);
1900
+ console.log(chalk11.bold("Status ") + a.status);
1901
+ console.log(chalk11.bold("Model ") + (a.model ?? "-"));
1902
+ console.log(chalk11.bold("Machine ") + (a.machine_size ?? "-"));
1903
+ if (a.fly_app_name) console.log(chalk11.bold("Fly App ") + a.fly_app_name);
1591
1904
  });
1592
1905
  }
1593
1906
 
1594
1907
  // src/commands/secrets.ts
1595
1908
  init_agent();
1596
- import chalk11 from "chalk";
1909
+ import chalk12 from "chalk";
1597
1910
  function registerSecretsCommands(program2) {
1598
1911
  const secrets = program2.command("secrets").description("Manage encrypted secrets vault for a Claw agent").addHelpText("after", `
1599
1912
  Secrets are encrypted key-value pairs stored in the agent's vault.
@@ -1629,11 +1942,11 @@ Examples:
1629
1942
  if (isJsonMode()) return printJson(result);
1630
1943
  const keys = result?.secrets?.map((s) => s.key) ?? result?.keys ?? [];
1631
1944
  if (!keys.length) {
1632
- console.log(chalk11.dim("(no secrets set \u2014 use `blink secrets set KEY value`)"));
1945
+ console.log(chalk12.dim("(no secrets set \u2014 use `blink secrets set KEY value`)"));
1633
1946
  return;
1634
1947
  }
1635
- for (const k of keys) console.log(chalk11.bold(k));
1636
- console.log(chalk11.dim(`
1948
+ for (const k of keys) console.log(chalk12.bold(k));
1949
+ console.log(chalk12.dim(`
1637
1950
  ${keys.length} secret${keys.length === 1 ? "" : "s"} (values hidden)`));
1638
1951
  });
1639
1952
  secrets.command("set <key> <value>").description("Add or update a secret (stored encrypted, value never shown again)").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID or BLINK_ACTIVE_AGENT)").addHelpText("after", `
@@ -1656,8 +1969,8 @@ After setting, the secret is available as $KEY_NAME in agent shell commands.
1656
1969
  );
1657
1970
  const normalised = key.toUpperCase();
1658
1971
  if (!isJsonMode()) {
1659
- console.log(chalk11.green("\u2713") + ` ${normalised} saved`);
1660
- console.log(chalk11.dim(" Value hidden. Use $" + normalised + " in shell commands."));
1972
+ console.log(chalk12.green("\u2713") + ` ${normalised} saved`);
1973
+ console.log(chalk12.dim(" Value hidden. Use $" + normalised + " in shell commands."));
1661
1974
  } else {
1662
1975
  printJson({ status: "ok", key: normalised, agent_id: agentId });
1663
1976
  }
@@ -1753,6 +2066,16 @@ Connectors (38 OAuth providers \u2014 GitHub, Notion, Slack, Stripe, Shopify, Ji
1753
2066
  $ blink connector exec linear '{ viewer { id name } }' POST GraphQL (Linear)
1754
2067
  Connect accounts at: blink.new/settings?tab=connectors
1755
2068
 
2069
+ LinkedIn (dedicated commands for the LinkedIn connector):
2070
+ $ blink linkedin me Show your LinkedIn profile
2071
+ $ blink linkedin posts List your 10 most recent posts
2072
+ $ blink linkedin post "Hello LinkedIn!" Publish a text post
2073
+ $ blink linkedin comments "urn:li:ugcPost:123" Read comments on a post
2074
+ $ blink linkedin comment "urn:li:ugcPost:123" "Nice!" Add a comment
2075
+ $ blink linkedin like "urn:li:ugcPost:123" Like a post
2076
+ $ blink linkedin unlike "urn:li:ugcPost:123" Unlike a post
2077
+ Link LinkedIn at: blink.new/claw (Agent Integrations tab)
2078
+
1756
2079
  Agents (Claw \u2014 zero config on Fly machines, BLINK_AGENT_ID is already set):
1757
2080
  $ blink agent list List all agents in workspace
1758
2081
  $ blink agent status Show current agent details
@@ -1791,6 +2114,7 @@ registerRealtimeCommands(program);
1791
2114
  registerRagCommands(program);
1792
2115
  registerNotifyCommands(program);
1793
2116
  registerConnectorCommands(program);
2117
+ registerLinkedInCommands(program);
1794
2118
  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", `
1795
2119
  Examples:
1796
2120
  $ blink use proj_xxx Shows the export command to run
@@ -1806,10 +2130,10 @@ After setting:
1806
2130
  process.stdout.write(`export BLINK_ACTIVE_PROJECT=${projectId}
1807
2131
  `);
1808
2132
  } else {
1809
- const { default: chalk12 } = await import("chalk");
1810
- console.log(chalk12.bold("Active project: ") + projectId);
1811
- console.log(chalk12.dim(`Run: export BLINK_ACTIVE_PROJECT=${projectId}`));
1812
- console.log(chalk12.dim(`Or: eval $(blink use ${projectId} --export)`));
2133
+ const { default: chalk13 } = await import("chalk");
2134
+ console.log(chalk13.bold("Active project: ") + projectId);
2135
+ console.log(chalk13.dim(`Run: export BLINK_ACTIVE_PROJECT=${projectId}`));
2136
+ console.log(chalk13.dim(`Or: eval $(blink use ${projectId} --export)`));
1813
2137
  }
1814
2138
  });
1815
2139
  program.action(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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
@@ -9,6 +9,7 @@ import { registerRealtimeCommands } from './commands/realtime.js'
9
9
  import { registerRagCommands } from './commands/rag.js'
10
10
  import { registerNotifyCommands } from './commands/notify.js'
11
11
  import { registerConnectorCommands } from './commands/connector.js'
12
+ import { registerLinkedInCommands } from './commands/linkedin.js'
12
13
  import { registerDeployCommands } from './commands/deploy.js'
13
14
  import { registerProjectCommands } from './commands/project.js'
14
15
  import { registerAuthCommands } from './commands/auth.js'
@@ -90,6 +91,16 @@ Connectors (38 OAuth providers — GitHub, Notion, Slack, Stripe, Shopify, Jira,
90
91
  $ blink connector exec linear '{ viewer { id name } }' POST GraphQL (Linear)
91
92
  Connect accounts at: blink.new/settings?tab=connectors
92
93
 
94
+ LinkedIn (dedicated commands for the LinkedIn connector):
95
+ $ blink linkedin me Show your LinkedIn profile
96
+ $ blink linkedin posts List your 10 most recent posts
97
+ $ blink linkedin post "Hello LinkedIn!" Publish a text post
98
+ $ blink linkedin comments "urn:li:ugcPost:123" Read comments on a post
99
+ $ blink linkedin comment "urn:li:ugcPost:123" "Nice!" Add a comment
100
+ $ blink linkedin like "urn:li:ugcPost:123" Like a post
101
+ $ blink linkedin unlike "urn:li:ugcPost:123" Unlike a post
102
+ Link LinkedIn at: blink.new/claw (Agent Integrations tab)
103
+
93
104
  Agents (Claw — zero config on Fly machines, BLINK_AGENT_ID is already set):
94
105
  $ blink agent list List all agents in workspace
95
106
  $ blink agent status Show current agent details
@@ -130,6 +141,7 @@ registerRealtimeCommands(program)
130
141
  registerRagCommands(program)
131
142
  registerNotifyCommands(program)
132
143
  registerConnectorCommands(program)
144
+ registerLinkedInCommands(program)
133
145
 
134
146
  program.command('use <project_id>')
135
147
  .description('Set active project for this shell session (alternative to blink link)')
@@ -42,6 +42,7 @@ Commands:
42
42
  animate Image-to-video animation (URL or local file)
43
43
  speech Text-to-speech (saves .mp3)
44
44
  transcribe Audio-to-text (file or URL)
45
+ call Make an AI phone call to any US/International number
45
46
 
46
47
  Examples:
47
48
  $ blink ai text "Write a product description for a CLI tool"
@@ -55,6 +56,7 @@ Examples:
55
56
  $ blink ai speech "Hello, welcome to Blink." --voice nova --output hello.mp3
56
57
  $ blink ai transcribe ./meeting.mp3 --language en
57
58
  $ blink ai transcribe https://example.com/audio.mp3
59
+ $ blink ai call "+14155551234" "Collect payment from John Smith"
58
60
 
59
61
  No project needed — AI commands are workspace-scoped.
60
62
  Add --json to any command for machine-readable output.
@@ -282,4 +284,95 @@ Supported formats: mp3, wav, m4a, ogg
282
284
  if (isJsonMode()) return printJson(result)
283
285
  console.log(result?.text ?? result?.transcript ?? JSON.stringify(result))
284
286
  })
287
+
288
+ ai.command('call <phone-number> <system-prompt>')
289
+ .description('Make an AI phone call to any number (US/International)')
290
+ .option('--voice <voice>', 'Voice: openai:alloy | openai:nova | cartesia:sonic-english', 'openai:alloy')
291
+ .option('--max-duration <seconds>', 'Max call duration in seconds', '300')
292
+ .option('--no-wait', 'Return call_id immediately without waiting for completion')
293
+ .addHelpText('after', `
294
+ Examples:
295
+ $ blink ai call "+14155551234" "You are collecting a payment of $240 from John Smith. Be polite but firm."
296
+ $ blink ai call "+14155551234" "Confirm John's appointment for tomorrow at 3pm" --voice openai:nova
297
+ $ blink ai call "+14155551234" "Leave a message about our new product launch" --no-wait
298
+ $ blink ai call "+14155551234" "Your task" --json | jq '.call_id'
299
+
300
+ Phone number must be in E.164 format: +1XXXXXXXXXX (US), +44XXXXXXXXXX (UK), etc.
301
+
302
+ Voices:
303
+ openai:alloy Balanced, neutral (default)
304
+ openai:nova Friendly, warm
305
+ openai:echo Clear, conversational
306
+ openai:onyx Deep, authoritative
307
+ openai:shimmer Soft, gentle
308
+ cartesia:sonic-english Low-latency, natural
309
+
310
+ Call is charged to your workspace credits after completion (~1 credit/min).
311
+ `)
312
+ .action(async (phoneNumber, systemPrompt, opts) => {
313
+ requireToken()
314
+ const result = await withSpinner('Initiating AI call...', () =>
315
+ resourcesRequest('/api/v1/ai/call', {
316
+ body: {
317
+ phone_number: phoneNumber,
318
+ system_prompt: systemPrompt,
319
+ voice: opts.voice,
320
+ max_duration_seconds: parseInt(opts.maxDuration),
321
+ },
322
+ })
323
+ )
324
+ if (isJsonMode()) return printJson(result)
325
+
326
+ const callId = result?.call_id as string
327
+
328
+ if (opts.noWait) {
329
+ console.log(`Call initiated: ${callId}`)
330
+ console.log(`Poll status: blink ai call-status ${callId}`)
331
+ return
332
+ }
333
+
334
+ // Poll until completed
335
+ console.log(`Call started → ${callId}`)
336
+ console.log(`Calling ${phoneNumber}...`)
337
+ let status = 'queued'
338
+ let attempts = 0
339
+ while (!['completed', 'failed', 'no-answer', 'busy'].includes(status) && attempts < 120) {
340
+ await new Promise(r => setTimeout(r, 5000))
341
+ attempts++
342
+ const poll = await resourcesRequest(`/api/v1/ai/call/${callId}`, { method: 'GET' }).catch(() => null)
343
+ if (poll) {
344
+ status = poll.status as string
345
+ if (['completed', 'failed', 'no-answer', 'busy'].includes(status)) {
346
+ if (status === 'completed') {
347
+ console.log(`\nCall completed (${poll.duration_seconds}s)`)
348
+ if (poll.transcript) console.log(`\nTranscript:\n${poll.transcript}`)
349
+ if (poll.credits_charged) console.log(`\nCredits charged: ${poll.credits_charged}`)
350
+ } else {
351
+ console.log(`\nCall ended: ${status}`)
352
+ }
353
+ return
354
+ }
355
+ }
356
+ }
357
+ console.log(`\nCall in progress. Check status: blink ai call-status ${callId}`)
358
+ })
359
+
360
+ ai.command('call-status <call-id>')
361
+ .description('Get status of an AI phone call')
362
+ .addHelpText('after', `
363
+ Examples:
364
+ $ blink ai call-status vc_a1b2c3d4
365
+ $ blink ai call-status vc_a1b2c3d4 --json
366
+ `)
367
+ .action(async (callId, _opts) => {
368
+ requireToken()
369
+ const result = await withSpinner('Fetching call status...', () =>
370
+ resourcesRequest(`/api/v1/ai/call/${callId}`, { method: 'GET' })
371
+ )
372
+ if (isJsonMode()) return printJson(result)
373
+ console.log(`Status: ${result?.status}`)
374
+ if (result?.duration_seconds) console.log(`Duration: ${result.duration_seconds}s`)
375
+ if (result?.transcript) console.log(`\nTranscript:\n${result.transcript}`)
376
+ if (result?.credits_charged) console.log(`Credits: ${result.credits_charged}`)
377
+ })
285
378
  }
@@ -65,7 +65,7 @@ Once linked, use \`blink connector exec\` to call their APIs without managing to
65
65
 
66
66
  Connect accounts at: https://blink.new/settings?tab=connectors
67
67
 
68
- Run \`blink connector providers\` to see all 39 supported providers.
68
+ Run \`blink connector providers\` to see all 38 supported providers.
69
69
 
70
70
  Quick examples:
71
71
  $ blink connector exec github /user/repos GET
@@ -0,0 +1,268 @@
1
+ import { Command } from 'commander'
2
+ import { resourcesRequest } from '../lib/api-resources.js'
3
+ import { requireToken } from '../lib/auth.js'
4
+ import { requireAgentId } from '../lib/agent.js'
5
+ import { printJson, isJsonMode, withSpinner } from '../lib/output.js'
6
+ import chalk from 'chalk'
7
+
8
+ const NOT_LINKED = 'LinkedIn not linked. Link it in the Agent Integrations tab at blink.new/claw'
9
+
10
+ async function liExec(
11
+ method: string,
12
+ httpMethod: string,
13
+ params: Record<string, unknown>,
14
+ agentId: string
15
+ ) {
16
+ const result = await resourcesRequest('/v1/connectors/linkedin/execute', {
17
+ body: { method, http_method: httpMethod, params },
18
+ headers: { 'x-blink-agent-id': agentId },
19
+ })
20
+ if (!result?.success) throw new Error(result?.error ?? NOT_LINKED)
21
+ return result.data
22
+ }
23
+
24
+ async function getPersonId(agentId: string): Promise<string> {
25
+ // Use OpenID Connect userinfo — more reliable than deprecated /v2/me
26
+ const data = await liExec('v2/userinfo', 'GET', {}, agentId)
27
+ const id = data?.sub ?? data?.id
28
+ if (!id) throw new Error('Could not resolve LinkedIn person ID')
29
+ return id
30
+ }
31
+
32
+ export function registerLinkedInCommands(program: Command) {
33
+ const li = program.command('linkedin')
34
+ .description('LinkedIn connector — post content, manage comments, and view your profile')
35
+ .addHelpText('after', `
36
+ LinkedIn must be linked to your agent via the Integrations tab at blink.new/claw.
37
+ Agent ID defaults to BLINK_AGENT_ID (automatically set on Claw Fly machines).
38
+
39
+ Examples:
40
+ $ blink linkedin me Show your LinkedIn profile
41
+ $ blink linkedin posts List your 10 most recent posts
42
+ $ blink linkedin post "Excited to announce..." Publish a text post
43
+ $ blink linkedin comments "urn:li:ugcPost:123" Read comments on a post
44
+ $ blink linkedin comment "urn:li:ugcPost:123" "Great post!" Add a comment
45
+ $ blink linkedin like "urn:li:ugcPost:123" Like a post
46
+ $ blink linkedin unlike "urn:li:ugcPost:123" Unlike a post
47
+ `)
48
+
49
+ // blink linkedin me
50
+ li.command('me')
51
+ .description('Show your LinkedIn profile (name, ID, vanity URL)')
52
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
53
+ .addHelpText('after', `
54
+ Examples:
55
+ $ blink linkedin me
56
+ $ blink linkedin me --json
57
+ $ blink linkedin me --agent clw_xxx
58
+ `)
59
+ .action(async (opts) => {
60
+ requireToken()
61
+ const agentId = requireAgentId(opts.agent)
62
+ // Use OpenID Connect userinfo endpoint — more reliable than /v2/me
63
+ const data = await withSpinner('Fetching LinkedIn profile...', () =>
64
+ liExec('v2/userinfo', 'GET', {}, agentId)
65
+ )
66
+ if (isJsonMode()) return printJson(data)
67
+ const name = data?.name ?? [data?.given_name, data?.family_name].filter(Boolean).join(' ')
68
+ const personId = data?.sub ?? data?.id
69
+ console.log(chalk.bold('LinkedIn Profile'))
70
+ if (personId) console.log(` ${chalk.dim('ID:')} ${personId}`)
71
+ if (name) console.log(` ${chalk.dim('Name:')} ${name}`)
72
+ if (data?.email) console.log(` ${chalk.dim('Email:')} ${data.email}`)
73
+ })
74
+
75
+ // blink linkedin posts
76
+ li.command('posts')
77
+ .description('List your most recent LinkedIn posts (last 10)')
78
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
79
+ .option('--limit <n>', 'Number of posts to fetch (default: 10)', '10')
80
+ .addHelpText('after', `
81
+ Examples:
82
+ $ blink linkedin posts
83
+ $ blink linkedin posts --limit 5
84
+ $ blink linkedin posts --json | jq '.[].id'
85
+ `)
86
+ .action(async (opts) => {
87
+ requireToken()
88
+ const agentId = requireAgentId(opts.agent)
89
+ const personId = await withSpinner('Resolving your LinkedIn identity...', () =>
90
+ getPersonId(agentId)
91
+ )
92
+ const urn = encodeURIComponent(`urn:li:person:${personId}`)
93
+ const data = await withSpinner('Fetching posts...', () =>
94
+ liExec(
95
+ `ugcPosts?q=authors&authors=List(${urn})&sortBy=LAST_MODIFIED&count=${opts.limit}`,
96
+ 'GET',
97
+ {},
98
+ agentId
99
+ )
100
+ )
101
+ const posts: Array<Record<string, unknown>> = data?.elements ?? (Array.isArray(data) ? data : [])
102
+ if (isJsonMode()) return printJson(posts)
103
+ if (!posts.length) { console.log(chalk.dim('No posts found.')); return }
104
+ for (const post of posts) {
105
+ const id = (post.id ?? '—') as string
106
+ const content = post.specificContent as Record<string, unknown> | undefined
107
+ const share = content?.['com.linkedin.ugc.ShareContent'] as Record<string, unknown> | undefined
108
+ const commentary = share?.shareCommentary as Record<string, unknown> | undefined
109
+ const text = (commentary?.text ?? (post.text as Record<string, unknown>)?.text ?? '(no text)') as string
110
+ const preview = text.length > 120 ? text.slice(0, 120) + '…' : text
111
+ console.log(chalk.bold(id))
112
+ console.log(` ${chalk.dim(preview)}`)
113
+ console.log()
114
+ }
115
+ console.log(chalk.dim(`${posts.length} post(s)`))
116
+ })
117
+
118
+ // blink linkedin post "text"
119
+ li.command('post <text>')
120
+ .description('Publish a text post to LinkedIn')
121
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
122
+ .option('--visibility <vis>', 'Post visibility: PUBLIC | CONNECTIONS (default: PUBLIC)', 'PUBLIC')
123
+ .addHelpText('after', `
124
+ Examples:
125
+ $ blink linkedin post "Excited to share our latest update!"
126
+ $ blink linkedin post "Internal update" --visibility CONNECTIONS
127
+ $ blink linkedin post "Hello LinkedIn" --json
128
+ `)
129
+ .action(async (text: string, opts) => {
130
+ requireToken()
131
+ const agentId = requireAgentId(opts.agent)
132
+ const data = await withSpinner('Publishing post...', () =>
133
+ liExec('/ugcPosts', 'POST', { text, visibility: opts.visibility }, agentId)
134
+ )
135
+ if (isJsonMode()) return printJson(data)
136
+ console.log(chalk.green('✓ Post published'))
137
+ if (data?.id) console.log(chalk.dim(` URN: ${data.id}`))
138
+ })
139
+
140
+ // blink linkedin comments <postUrn>
141
+ li.command('comments <postUrn>')
142
+ .description('Read comments on a LinkedIn post')
143
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
144
+ .addHelpText('after', `
145
+ <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
146
+ Use "blink linkedin posts" to find your post URNs.
147
+
148
+ Examples:
149
+ $ blink linkedin comments "urn:li:ugcPost:1234567890"
150
+ $ blink linkedin comments "urn:li:ugcPost:1234567890" --json
151
+ `)
152
+ .action(async (postUrn: string, opts) => {
153
+ requireToken()
154
+ const agentId = requireAgentId(opts.agent)
155
+ const encoded = encodeURIComponent(postUrn)
156
+ const data = await withSpinner('Fetching comments...', () =>
157
+ liExec(`rest/socialActions/${encoded}/comments`, 'GET', {}, agentId)
158
+ )
159
+ const comments: Array<Record<string, unknown>> = data?.elements ?? (Array.isArray(data) ? data : [])
160
+ if (isJsonMode()) return printJson(comments)
161
+ if (!comments.length) { console.log(chalk.dim('No comments.')); return }
162
+ for (const c of comments) {
163
+ const author = (c.actor ?? '—') as string
164
+ const msg = c.message as Record<string, unknown> | undefined
165
+ const text = (msg?.text ?? '(no text)') as string
166
+ console.log(chalk.bold(author))
167
+ console.log(` ${text}`)
168
+ console.log()
169
+ }
170
+ console.log(chalk.dim(`${comments.length} comment(s)`))
171
+ })
172
+
173
+ // blink linkedin comment <postUrn> "text"
174
+ li.command('comment <postUrn> <text>')
175
+ .description('Add a comment to a LinkedIn post')
176
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
177
+ .addHelpText('after', `
178
+ <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
179
+
180
+ Examples:
181
+ $ blink linkedin comment "urn:li:ugcPost:1234567890" "Great post!"
182
+ $ blink linkedin comment "urn:li:ugcPost:1234567890" "Thanks for sharing" --json
183
+ `)
184
+ .action(async (postUrn: string, text: string, opts) => {
185
+ requireToken()
186
+ const agentId = requireAgentId(opts.agent)
187
+ const personId = await withSpinner('Resolving your LinkedIn identity...', () =>
188
+ getPersonId(agentId)
189
+ )
190
+ const actor = `urn:li:person:${personId}`
191
+ const encoded = encodeURIComponent(postUrn)
192
+ const data = await withSpinner('Adding comment...', () =>
193
+ liExec(
194
+ `rest/socialActions/${encoded}/comments`,
195
+ 'POST',
196
+ { actor, object: postUrn, message: { text } },
197
+ agentId
198
+ )
199
+ )
200
+ if (isJsonMode()) return printJson(data)
201
+ console.log(chalk.green('✓ Comment added'))
202
+ if (data?.id) console.log(chalk.dim(` ID: ${data.id}`))
203
+ })
204
+
205
+ // blink linkedin like <postUrn>
206
+ li.command('like <postUrn>')
207
+ .description('Like a LinkedIn post')
208
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
209
+ .addHelpText('after', `
210
+ <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
211
+
212
+ Examples:
213
+ $ blink linkedin like "urn:li:ugcPost:1234567890"
214
+ $ blink linkedin like "urn:li:ugcPost:1234567890" --json
215
+ `)
216
+ .action(async (postUrn: string, opts) => {
217
+ requireToken()
218
+ const agentId = requireAgentId(opts.agent)
219
+ const personId = await withSpinner('Resolving your LinkedIn identity...', () =>
220
+ getPersonId(agentId)
221
+ )
222
+ const actor = `urn:li:person:${personId}`
223
+ const encoded = encodeURIComponent(postUrn)
224
+ const data = await withSpinner('Liking post...', () =>
225
+ liExec(
226
+ `rest/socialActions/${encoded}/likes`,
227
+ 'POST',
228
+ { actor, object: postUrn },
229
+ agentId
230
+ )
231
+ )
232
+ if (isJsonMode()) return printJson(data)
233
+ console.log(chalk.green('✓ Post liked'))
234
+ })
235
+
236
+ // blink linkedin unlike <postUrn>
237
+ li.command('unlike <postUrn>')
238
+ .description('Unlike a LinkedIn post')
239
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
240
+ .addHelpText('after', `
241
+ <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
242
+
243
+ Examples:
244
+ $ blink linkedin unlike "urn:li:ugcPost:1234567890"
245
+ $ blink linkedin unlike "urn:li:ugcPost:1234567890" --json
246
+ `)
247
+ .action(async (postUrn: string, opts) => {
248
+ requireToken()
249
+ const agentId = requireAgentId(opts.agent)
250
+ const personId = await withSpinner('Resolving your LinkedIn identity...', () =>
251
+ getPersonId(agentId)
252
+ )
253
+ const actor = `urn:li:person:${personId}`
254
+ const encodedPost = encodeURIComponent(postUrn)
255
+ const encodedActor = encodeURIComponent(actor)
256
+ // LinkedIn DELETE likes requires: DELETE /rest/socialActions/{postUrn}/likes/{actorUrn}?actor={actorUrn}
257
+ const data = await withSpinner('Unliking post...', () =>
258
+ liExec(
259
+ `rest/socialActions/${encodedPost}/likes/${encodedActor}?actor=${encodedActor}`,
260
+ 'DELETE',
261
+ {},
262
+ agentId
263
+ )
264
+ )
265
+ if (isJsonMode()) return printJson(data)
266
+ console.log(chalk.green('✓ Post unliked'))
267
+ })
268
+ }