@blinkdotnew/cli 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -478,7 +478,7 @@ Supported formats: mp3, wav, m4a, ogg
478
478
  if (isJsonMode()) return printJson(result);
479
479
  console.log(result?.text ?? result?.transcript ?? JSON.stringify(result));
480
480
  });
481
- ai.command("call <phone-number> <system-prompt>").description("Make an AI phone call to any number (US/International)").option("--voice <voice>", "Voice: openai:alloy | openai:nova | cartesia:sonic-english", "openai:alloy").option("--max-duration <seconds>", "Max call duration in seconds", "300").option("--no-wait", "Return call_id immediately without waiting for completion").addHelpText("after", `
481
+ ai.command("call <phone-number> <system-prompt>").description("Make an AI phone call to any number (US/International)").option("--voice <voice>", "Voice: openai:alloy | openai:nova | cartesia:sonic-english", "openai:alloy").option("--max-duration <seconds>", "Max call duration in seconds", "300").option("--no-wait", "Return call_id immediately without waiting for completion").option("--from <number>", "Phone number to call from (E.164 format, e.g. +14155551234). Uses primary number if omitted.").addHelpText("after", `
482
482
  Examples:
483
483
  $ blink ai call "+14155551234" "You are collecting a payment of $240 from John Smith. Be polite but firm."
484
484
  $ blink ai call "+14155551234" "Confirm John's appointment for tomorrow at 3pm" --voice openai:nova
@@ -496,6 +496,7 @@ Voices:
496
496
  cartesia:sonic-english Low-latency, natural
497
497
 
498
498
  Call is charged to your workspace credits after completion (~1 credit/min).
499
+ Use --from to specify which of your workspace numbers to call from.
499
500
  `).action(async (phoneNumber, systemPrompt, opts) => {
500
501
  requireToken();
501
502
  const result = await withSpinner(
@@ -505,13 +506,14 @@ Call is charged to your workspace credits after completion (~1 credit/min).
505
506
  phone_number: phoneNumber,
506
507
  system_prompt: systemPrompt,
507
508
  voice: opts.voice,
508
- max_duration_seconds: parseInt(opts.maxDuration)
509
+ max_duration_seconds: parseInt(opts.maxDuration),
510
+ ...opts.from ? { from_number: opts.from } : {}
509
511
  }
510
512
  })
511
513
  );
512
514
  if (isJsonMode()) return printJson(result);
513
515
  const callId = result?.call_id;
514
- if (opts.noWait) {
516
+ if (!opts.wait) {
515
517
  console.log(`Call initiated: ${callId}`);
516
518
  console.log(`Poll status: blink ai call-status ${callId}`);
517
519
  return;
@@ -1227,7 +1229,7 @@ ${filtered.length} providers total. Connect at blink.new/settings?tab=connectors
1227
1229
  ${connected.length} provider(s) connected`));
1228
1230
  }
1229
1231
  });
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", `
1232
+ 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
1233
  <endpoint> is the API path relative to the provider's base URL, OR a GraphQL query string for Linear.
1232
1234
 
1233
1235
  Examples (REST):
@@ -1260,22 +1262,35 @@ Provider base URLs used:
1260
1262
  jira https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3/
1261
1263
  shopify https://{shop}.myshopify.com/admin/api/2024-10/
1262
1264
  ... run \`blink connector providers\` for all 38 providers
1263
- `).action(async (provider, endpoint, paramsArg, opts) => {
1265
+ `).action(async (provider, endpoint, arg1, arg2, opts) => {
1264
1266
  requireToken();
1267
+ const HTTP_METHODS = /^(GET|POST|PUT|PATCH|DELETE|HEAD)$/i;
1268
+ let httpMethod = opts.method;
1265
1269
  let params = {};
1266
- if (paramsArg) {
1267
- try {
1268
- params = JSON.parse(paramsArg);
1269
- } catch {
1270
- params = {};
1270
+ if (arg1) {
1271
+ if (HTTP_METHODS.test(arg1)) {
1272
+ httpMethod = arg1.toUpperCase();
1273
+ if (arg2) {
1274
+ try {
1275
+ params = JSON.parse(arg2);
1276
+ } catch {
1277
+ params = {};
1278
+ }
1279
+ }
1280
+ } else {
1281
+ try {
1282
+ params = JSON.parse(arg1);
1283
+ } catch {
1284
+ params = {};
1285
+ }
1271
1286
  }
1272
1287
  }
1273
1288
  const result = await withSpinner(
1274
- `${opts.method} ${provider}${endpoint}...`,
1289
+ `${httpMethod} ${provider}${endpoint}...`,
1275
1290
  () => resourcesRequest(`/v1/connectors/${provider}/execute`, {
1276
1291
  body: {
1277
1292
  method: endpoint,
1278
- http_method: opts.method,
1293
+ http_method: httpMethod,
1279
1294
  params,
1280
1295
  ...opts.account ? { account_id: opts.account } : {}
1281
1296
  }
@@ -1305,20 +1320,26 @@ async function getPersonId(agentId) {
1305
1320
  return id;
1306
1321
  }
1307
1322
  function registerLinkedInCommands(program2) {
1308
- const li = program2.command("linkedin").description("LinkedIn connector \u2014 post content, manage comments, and view your profile").addHelpText("after", `
1323
+ const li = program2.command("linkedin").description("LinkedIn connector \u2014 publish posts and manage your profile").addHelpText("after", `
1309
1324
  LinkedIn must be linked to your agent via the Integrations tab at blink.new/claw.
1310
1325
  Agent ID defaults to BLINK_AGENT_ID (automatically set on Claw Fly machines).
1311
1326
 
1327
+ What works today (w_member_social scope):
1328
+ \u2705 blink linkedin me Show your LinkedIn profile
1329
+ \u2705 blink linkedin post "Excited to announce..." Publish a text post
1330
+ \u2705 blink linkedin delete <postUrn> Delete one of your posts
1331
+
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)
1335
+
1312
1336
  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
1337
+ $ 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"
1320
1341
  `);
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", `
1342
+ li.command("me").description("Show your LinkedIn profile (name, ID, email)").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1322
1343
  Examples:
1323
1344
  $ blink linkedin me
1324
1345
  $ blink linkedin me --json
@@ -1338,57 +1359,16 @@ Examples:
1338
1359
  if (name) console.log(` ${chalk7.dim("Name:")} ${name}`);
1339
1360
  if (data?.email) console.log(` ${chalk7.dim("Email:")} ${data.email}`);
1340
1361
  });
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 authorUrn = encodeURIComponent(`urn:li:person:${personId}`);
1354
- const data = await withSpinner(
1355
- "Fetching posts...",
1356
- () => liExec(
1357
- `ugcPosts?q=authors&authors=List(${authorUrn})&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 ?? "(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", `
1362
+ 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", `
1383
1363
  Examples:
1384
1364
  $ blink linkedin post "Excited to share our latest update!"
1385
- $ blink linkedin post "Internal update" --visibility CONNECTIONS
1365
+ $ blink linkedin post "Internal team update" --visibility CONNECTIONS
1386
1366
  $ blink linkedin post "Hello LinkedIn" --json
1387
1367
  `).action(async (text, opts) => {
1388
1368
  requireToken();
1389
1369
  const agentId = requireAgentId(opts.agent);
1390
1370
  const personId = await withSpinner(
1391
- "Resolving your LinkedIn identity...",
1371
+ "Resolving LinkedIn identity...",
1392
1372
  () => getPersonId(agentId)
1393
1373
  );
1394
1374
  const visibilityMap = {
@@ -1415,119 +1395,46 @@ Examples:
1415
1395
  console.log(chalk7.green("\u2713 Post published"));
1416
1396
  if (data?.id) console.log(chalk7.dim(` URN: ${data.id}`));
1417
1397
  });
1418
- li.command("comments <postUrn>").description("Read comments on a LinkedIn post").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1419
- <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
1420
- Use "blink linkedin posts" to find your post URNs.
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.
1421
1400
 
1422
1401
  Examples:
1423
- $ blink linkedin comments "urn:li:ugcPost:1234567890"
1424
- $ blink linkedin comments "urn:li:ugcPost:1234567890" --json
1425
- `).action(async (postUrn, opts) => {
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) => {
1426
1406
  requireToken();
1427
1407
  const agentId = requireAgentId(opts.agent);
1428
- const encoded = encodeURIComponent(postUrn);
1429
- const data = await withSpinner(
1430
- "Fetching comments...",
1431
- () => liExec(`rest/socialActions/${encoded}/comments`, "GET", {}, agentId)
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
+ })
1432
1414
  );
1433
- const comments = data?.elements ?? (Array.isArray(data) ? data : []);
1434
- if (isJsonMode()) return printJson(comments);
1435
- if (!comments.length) {
1436
- console.log(chalk7.dim("No comments."));
1437
- return;
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}`));
1438
1420
  }
1439
- for (const c of comments) {
1440
- const author = c.actor ?? "\u2014";
1441
- const msg = c.message;
1442
- const text = msg?.text ?? "(no text)";
1443
- console.log(chalk7.bold(author));
1444
- console.log(` ${text}`);
1445
- console.log();
1446
- }
1447
- console.log(chalk7.dim(`${comments.length} comment(s)`));
1448
- });
1449
- 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", `
1450
- <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
1451
-
1452
- Examples:
1453
- $ blink linkedin comment "urn:li:ugcPost:1234567890" "Great post!"
1454
- $ blink linkedin comment "urn:li:ugcPost:1234567890" "Thanks for sharing" --json
1455
- `).action(async (postUrn, text, opts) => {
1456
- requireToken();
1457
- const agentId = requireAgentId(opts.agent);
1458
- const personId = await withSpinner(
1459
- "Resolving your LinkedIn identity...",
1460
- () => getPersonId(agentId)
1461
- );
1462
- const actor = `urn:li:person:${personId}`;
1463
- const encoded = encodeURIComponent(postUrn);
1464
- const data = await withSpinner(
1465
- "Adding comment...",
1466
- () => liExec(
1467
- `rest/socialActions/${encoded}/comments`,
1468
- "POST",
1469
- { actor, object: postUrn, message: { text } },
1470
- agentId
1471
- )
1472
- );
1473
- if (isJsonMode()) return printJson(data);
1474
- console.log(chalk7.green("\u2713 Comment added"));
1475
- if (data?.id) console.log(chalk7.dim(` ID: ${data.id}`));
1476
1421
  });
1477
- li.command("like <postUrn>").description("Like a LinkedIn post").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1478
- <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
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
1479
1425
 
1480
1426
  Examples:
1481
- $ blink linkedin like "urn:li:ugcPost:1234567890"
1482
- $ blink linkedin like "urn:li:ugcPost:1234567890" --json
1427
+ $ blink linkedin delete "urn:li:ugcPost:1234567890"
1483
1428
  `).action(async (postUrn, opts) => {
1484
1429
  requireToken();
1485
1430
  const agentId = requireAgentId(opts.agent);
1486
- const personId = await withSpinner(
1487
- "Resolving your LinkedIn identity...",
1488
- () => getPersonId(agentId)
1489
- );
1490
- const actor = `urn:li:person:${personId}`;
1491
1431
  const encoded = encodeURIComponent(postUrn);
1492
- const data = await withSpinner(
1493
- "Liking post...",
1494
- () => liExec(
1495
- `rest/socialActions/${encoded}/likes`,
1496
- "POST",
1497
- { actor, object: postUrn },
1498
- agentId
1499
- )
1500
- );
1501
- if (isJsonMode()) return printJson(data);
1502
- console.log(chalk7.green("\u2713 Post liked"));
1503
- });
1504
- li.command("unlike <postUrn>").description("Unlike a LinkedIn post").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1505
- <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
1506
-
1507
- Examples:
1508
- $ blink linkedin unlike "urn:li:ugcPost:1234567890"
1509
- $ blink linkedin unlike "urn:li:ugcPost:1234567890" --json
1510
- `).action(async (postUrn, opts) => {
1511
- requireToken();
1512
- const agentId = requireAgentId(opts.agent);
1513
- const personId = await withSpinner(
1514
- "Resolving your LinkedIn identity...",
1515
- () => getPersonId(agentId)
1516
- );
1517
- const actor = `urn:li:person:${personId}`;
1518
- const encodedPost = encodeURIComponent(postUrn);
1519
- const encodedActor = encodeURIComponent(actor);
1520
- const data = await withSpinner(
1521
- "Unliking post...",
1522
- () => liExec(
1523
- `rest/socialActions/${encodedPost}/likes/${encodedActor}?actor=${encodedActor}`,
1524
- "DELETE",
1525
- {},
1526
- agentId
1527
- )
1432
+ await withSpinner(
1433
+ "Deleting post...",
1434
+ () => liExec(`ugcPosts/${encoded}`, "DELETE", {}, agentId)
1528
1435
  );
1529
- if (isJsonMode()) return printJson(data);
1530
- console.log(chalk7.green("\u2713 Post unliked"));
1436
+ if (isJsonMode()) return printJson({ deleted: true, urn: postUrn });
1437
+ console.log(chalk7.green("\u2713 Post deleted"));
1531
1438
  });
1532
1439
  }
1533
1440
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/cli",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "Blink platform CLI — deploy apps, manage databases, generate AI content",
5
5
  "bin": {
6
6
  "blink": "dist/cli.js"
@@ -290,6 +290,7 @@ Supported formats: mp3, wav, m4a, ogg
290
290
  .option('--voice <voice>', 'Voice: openai:alloy | openai:nova | cartesia:sonic-english', 'openai:alloy')
291
291
  .option('--max-duration <seconds>', 'Max call duration in seconds', '300')
292
292
  .option('--no-wait', 'Return call_id immediately without waiting for completion')
293
+ .option('--from <number>', 'Phone number to call from (E.164 format, e.g. +14155551234). Uses primary number if omitted.')
293
294
  .addHelpText('after', `
294
295
  Examples:
295
296
  $ blink ai call "+14155551234" "You are collecting a payment of $240 from John Smith. Be polite but firm."
@@ -308,6 +309,7 @@ Voices:
308
309
  cartesia:sonic-english Low-latency, natural
309
310
 
310
311
  Call is charged to your workspace credits after completion (~1 credit/min).
312
+ Use --from to specify which of your workspace numbers to call from.
311
313
  `)
312
314
  .action(async (phoneNumber, systemPrompt, opts) => {
313
315
  requireToken()
@@ -318,6 +320,7 @@ Call is charged to your workspace credits after completion (~1 credit/min).
318
320
  system_prompt: systemPrompt,
319
321
  voice: opts.voice,
320
322
  max_duration_seconds: parseInt(opts.maxDuration),
323
+ ...(opts.from ? { from_number: opts.from } : {}),
321
324
  },
322
325
  })
323
326
  )
@@ -325,7 +328,7 @@ Call is charged to your workspace credits after completion (~1 credit/min).
325
328
 
326
329
  const callId = result?.call_id as string
327
330
 
328
- if (opts.noWait) {
331
+ if (!opts.wait) { // Commander --no-wait sets opts.wait=false (not opts.noWait)
329
332
  console.log(`Call initiated: ${callId}`)
330
333
  console.log(`Poll status: blink ai call-status ${callId}`)
331
334
  return
@@ -149,8 +149,12 @@ Use --account <id> if you have multiple linked accounts for the same provider.
149
149
  }
150
150
  })
151
151
 
152
- // blink connector exec <provider> <endpoint> [params]
153
- connector.command('exec <provider> <endpoint> [params]')
152
+ // blink connector exec <provider> <endpoint> [method-or-params] [params]
153
+ // Supports both patterns:
154
+ // blink connector exec github /user/repos GET
155
+ // blink connector exec notion /search POST '{"query":"notes"}'
156
+ // blink connector exec notion /search '{"query":"notes"}'
157
+ connector.command('exec <provider> <endpoint> [method-or-params] [params]')
154
158
  .description('Execute a call on a connected OAuth provider')
155
159
  .option('--account <id>', 'Specific account ID (if you have multiple accounts)')
156
160
  .option('--method <method>', 'HTTP method: GET | POST | PUT | PATCH | DELETE (default: POST)', 'POST')
@@ -188,17 +192,28 @@ Provider base URLs used:
188
192
  shopify https://{shop}.myshopify.com/admin/api/2024-10/
189
193
  ... run \`blink connector providers\` for all 38 providers
190
194
  `)
191
- .action(async (provider: string, endpoint: string, paramsArg: string | undefined, opts) => {
195
+ .action(async (provider: string, endpoint: string, arg1: string | undefined, arg2: string | undefined, opts) => {
192
196
  requireToken()
197
+ const HTTP_METHODS = /^(GET|POST|PUT|PATCH|DELETE|HEAD)$/i
198
+ let httpMethod = opts.method
193
199
  let params: Record<string, unknown> = {}
194
- if (paramsArg) {
195
- try { params = JSON.parse(paramsArg) } catch { params = {} }
200
+
201
+ if (arg1) {
202
+ if (HTTP_METHODS.test(arg1)) {
203
+ // Pattern: exec provider endpoint GET ['{"key":"val"}']
204
+ httpMethod = arg1.toUpperCase()
205
+ if (arg2) { try { params = JSON.parse(arg2) } catch { params = {} } }
206
+ } else {
207
+ // Pattern: exec provider endpoint '{"key":"val"}'
208
+ try { params = JSON.parse(arg1) } catch { params = {} }
209
+ }
196
210
  }
197
- const result = await withSpinner(`${opts.method} ${provider}${endpoint}...`, () =>
211
+
212
+ const result = await withSpinner(`${httpMethod} ${provider}${endpoint}...`, () =>
198
213
  resourcesRequest(`/v1/connectors/${provider}/execute`, {
199
214
  body: {
200
215
  method: endpoint,
201
- http_method: opts.method,
216
+ http_method: httpMethod,
202
217
  params,
203
218
  ...(opts.account ? { account_id: opts.account } : {}),
204
219
  }
@@ -1,8 +1,8 @@
1
1
  import { Command } from 'commander'
2
- import { resourcesRequest } from '../lib/api-resources.js'
3
2
  import { requireToken } from '../lib/auth.js'
4
3
  import { requireAgentId } from '../lib/agent.js'
5
4
  import { printJson, isJsonMode, withSpinner } from '../lib/output.js'
5
+ import { resourcesRequest } from '../lib/api-resources.js'
6
6
  import chalk from 'chalk'
7
7
 
8
8
  const NOT_LINKED = 'LinkedIn not linked. Link it in the Agent Integrations tab at blink.new/claw'
@@ -22,7 +22,6 @@ async function liExec(
22
22
  }
23
23
 
24
24
  async function getPersonId(agentId: string): Promise<string> {
25
- // Use OpenID Connect userinfo — more reliable than deprecated /v2/me
26
25
  const data = await liExec('v2/userinfo', 'GET', {}, agentId)
27
26
  const id = data?.sub ?? data?.id
28
27
  if (!id) throw new Error('Could not resolve LinkedIn person ID')
@@ -31,24 +30,30 @@ async function getPersonId(agentId: string): Promise<string> {
31
30
 
32
31
  export function registerLinkedInCommands(program: Command) {
33
32
  const li = program.command('linkedin')
34
- .description('LinkedIn connector — post content, manage comments, and view your profile')
33
+ .description('LinkedIn connector — publish posts and manage your profile')
35
34
  .addHelpText('after', `
36
35
  LinkedIn must be linked to your agent via the Integrations tab at blink.new/claw.
37
36
  Agent ID defaults to BLINK_AGENT_ID (automatically set on Claw Fly machines).
38
37
 
38
+ What works today (w_member_social scope):
39
+ ✅ blink linkedin me Show your LinkedIn profile
40
+ ✅ blink linkedin post "Excited to announce..." Publish a text post
41
+ ✅ blink linkedin delete <postUrn> Delete one of your posts
42
+
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
+
39
47
  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
48
+ $ 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"
47
52
  `)
48
53
 
49
54
  // blink linkedin me
50
55
  li.command('me')
51
- .description('Show your LinkedIn profile (name, ID, vanity URL)')
56
+ .description('Show your LinkedIn profile (name, ID, email)')
52
57
  .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
53
58
  .addHelpText('after', `
54
59
  Examples:
@@ -59,7 +64,6 @@ Examples:
59
64
  .action(async (opts) => {
60
65
  requireToken()
61
66
  const agentId = requireAgentId(opts.agent)
62
- // Use OpenID Connect userinfo endpoint — more reliable than /v2/me
63
67
  const data = await withSpinner('Fetching LinkedIn profile...', () =>
64
68
  liExec('v2/userinfo', 'GET', {}, agentId)
65
69
  )
@@ -67,69 +71,26 @@ Examples:
67
71
  const name = data?.name ?? [data?.given_name, data?.family_name].filter(Boolean).join(' ')
68
72
  const personId = data?.sub ?? data?.id
69
73
  console.log(chalk.bold('LinkedIn Profile'))
70
- if (personId) console.log(` ${chalk.dim('ID:')} ${personId}`)
71
- if (name) console.log(` ${chalk.dim('Name:')} ${name}`)
74
+ if (personId) console.log(` ${chalk.dim('ID:')} ${personId}`)
75
+ if (name) console.log(` ${chalk.dim('Name:')} ${name}`)
72
76
  if (data?.email) console.log(` ${chalk.dim('Email:')} ${data.email}`)
73
77
  })
74
78
 
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 authorUrn = encodeURIComponent(`urn:li:person:${personId}`)
93
- const data = await withSpinner('Fetching posts...', () =>
94
- liExec(
95
- `ugcPosts?q=authors&authors=List(${authorUrn})&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 ?? '(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
79
  // blink linkedin post "text"
119
80
  li.command('post <text>')
120
81
  .description('Publish a text post to LinkedIn')
121
82
  .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
122
- .option('--visibility <vis>', 'Post visibility: PUBLIC | CONNECTIONS (default: PUBLIC)', 'PUBLIC')
83
+ .option('--visibility <vis>', 'PUBLIC | CONNECTIONS (default: PUBLIC)', 'PUBLIC')
123
84
  .addHelpText('after', `
124
85
  Examples:
125
86
  $ blink linkedin post "Excited to share our latest update!"
126
- $ blink linkedin post "Internal update" --visibility CONNECTIONS
87
+ $ blink linkedin post "Internal team update" --visibility CONNECTIONS
127
88
  $ blink linkedin post "Hello LinkedIn" --json
128
89
  `)
129
90
  .action(async (text: string, opts) => {
130
91
  requireToken()
131
92
  const agentId = requireAgentId(opts.agent)
132
- const personId = await withSpinner('Resolving your LinkedIn identity...', () =>
93
+ const personId = await withSpinner('Resolving LinkedIn identity...', () =>
133
94
  getPersonId(agentId)
134
95
  )
135
96
  const visibilityMap: Record<string, string> = {
@@ -156,132 +117,55 @@ Examples:
156
117
  if (data?.id) console.log(chalk.dim(` URN: ${data.id}`))
157
118
  })
158
119
 
159
- // blink linkedin comments <postUrn>
160
- li.command('comments <postUrn>')
161
- .description('Read comments on a LinkedIn post')
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')
162
124
  .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
163
125
  .addHelpText('after', `
164
- <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
165
- Use "blink linkedin posts" to find your post URNs.
126
+ Returns an asset URN to use when composing a post with media via blink connector exec.
166
127
 
167
128
  Examples:
168
- $ blink linkedin comments "urn:li:ugcPost:1234567890"
169
- $ blink linkedin comments "urn:li:ugcPost:1234567890" --json
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'])")
170
132
  `)
171
- .action(async (postUrn: string, opts) => {
133
+ .action(async (mediaUrl: string, opts) => {
172
134
  requireToken()
173
135
  const agentId = requireAgentId(opts.agent)
174
- const encoded = encodeURIComponent(postUrn)
175
- const data = await withSpinner('Fetching comments...', () =>
176
- liExec(`rest/socialActions/${encoded}/comments`, 'GET', {}, agentId)
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
+ })
177
141
  )
178
- const comments: Array<Record<string, unknown>> = data?.elements ?? (Array.isArray(data) ? data : [])
179
- if (isJsonMode()) return printJson(comments)
180
- if (!comments.length) { console.log(chalk.dim('No comments.')); return }
181
- for (const c of comments) {
182
- const author = (c.actor ?? '—') as string
183
- const msg = c.message as Record<string, unknown> | undefined
184
- const text = (msg?.text ?? '(no text)') as string
185
- console.log(chalk.bold(author))
186
- console.log(` ${text}`)
187
- console.log()
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}`))
188
147
  }
189
- console.log(chalk.dim(`${comments.length} comment(s)`))
190
148
  })
191
149
 
192
- // blink linkedin comment <postUrn> "text"
193
- li.command('comment <postUrn> <text>')
194
- .description('Add a comment to a LinkedIn post')
150
+ // blink linkedin delete <postUrn>
151
+ li.command('delete <postUrn>')
152
+ .description('Delete one of your LinkedIn posts')
195
153
  .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
196
154
  .addHelpText('after', `
197
- <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
155
+ <postUrn> is the LinkedIn post URN returned when the post was created.
156
+ e.g. urn:li:ugcPost:1234567890
198
157
 
199
158
  Examples:
200
- $ blink linkedin comment "urn:li:ugcPost:1234567890" "Great post!"
201
- $ blink linkedin comment "urn:li:ugcPost:1234567890" "Thanks for sharing" --json
202
- `)
203
- .action(async (postUrn: string, text: string, opts) => {
204
- requireToken()
205
- const agentId = requireAgentId(opts.agent)
206
- const personId = await withSpinner('Resolving your LinkedIn identity...', () =>
207
- getPersonId(agentId)
208
- )
209
- const actor = `urn:li:person:${personId}`
210
- const encoded = encodeURIComponent(postUrn)
211
- const data = await withSpinner('Adding comment...', () =>
212
- liExec(
213
- `rest/socialActions/${encoded}/comments`,
214
- 'POST',
215
- { actor, object: postUrn, message: { text } },
216
- agentId
217
- )
218
- )
219
- if (isJsonMode()) return printJson(data)
220
- console.log(chalk.green('✓ Comment added'))
221
- if (data?.id) console.log(chalk.dim(` ID: ${data.id}`))
222
- })
223
-
224
- // blink linkedin like <postUrn>
225
- li.command('like <postUrn>')
226
- .description('Like a LinkedIn post')
227
- .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
228
- .addHelpText('after', `
229
- <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
230
-
231
- Examples:
232
- $ blink linkedin like "urn:li:ugcPost:1234567890"
233
- $ blink linkedin like "urn:li:ugcPost:1234567890" --json
159
+ $ blink linkedin delete "urn:li:ugcPost:1234567890"
234
160
  `)
235
161
  .action(async (postUrn: string, opts) => {
236
162
  requireToken()
237
163
  const agentId = requireAgentId(opts.agent)
238
- const personId = await withSpinner('Resolving your LinkedIn identity...', () =>
239
- getPersonId(agentId)
240
- )
241
- const actor = `urn:li:person:${personId}`
242
164
  const encoded = encodeURIComponent(postUrn)
243
- const data = await withSpinner('Liking post...', () =>
244
- liExec(
245
- `rest/socialActions/${encoded}/likes`,
246
- 'POST',
247
- { actor, object: postUrn },
248
- agentId
249
- )
165
+ await withSpinner('Deleting post...', () =>
166
+ liExec(`ugcPosts/${encoded}`, 'DELETE', {}, agentId)
250
167
  )
251
- if (isJsonMode()) return printJson(data)
252
- console.log(chalk.green('✓ Post liked'))
253
- })
254
-
255
- // blink linkedin unlike <postUrn>
256
- li.command('unlike <postUrn>')
257
- .description('Unlike a LinkedIn post')
258
- .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
259
- .addHelpText('after', `
260
- <postUrn> is the LinkedIn post URN, e.g. urn:li:ugcPost:1234567890
261
-
262
- Examples:
263
- $ blink linkedin unlike "urn:li:ugcPost:1234567890"
264
- $ blink linkedin unlike "urn:li:ugcPost:1234567890" --json
265
- `)
266
- .action(async (postUrn: string, opts) => {
267
- requireToken()
268
- const agentId = requireAgentId(opts.agent)
269
- const personId = await withSpinner('Resolving your LinkedIn identity...', () =>
270
- getPersonId(agentId)
271
- )
272
- const actor = `urn:li:person:${personId}`
273
- const encodedPost = encodeURIComponent(postUrn)
274
- const encodedActor = encodeURIComponent(actor)
275
- // LinkedIn DELETE likes requires: DELETE /rest/socialActions/{postUrn}/likes/{actorUrn}?actor={actorUrn}
276
- const data = await withSpinner('Unliking post...', () =>
277
- liExec(
278
- `rest/socialActions/${encodedPost}/likes/${encodedActor}?actor=${encodedActor}`,
279
- 'DELETE',
280
- {},
281
- agentId
282
- )
283
- )
284
- if (isJsonMode()) return printJson(data)
285
- console.log(chalk.green('✓ Post unliked'))
168
+ if (isJsonMode()) return printJson({ deleted: true, urn: postUrn })
169
+ console.log(chalk.green('✓ Post deleted'))
286
170
  })
287
171
  }