@blinkdotnew/cli 0.3.1 → 0.3.3

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
@@ -1127,6 +1127,7 @@ The email is sent from your project's configured sender address (set in blink.ne
1127
1127
  }
1128
1128
 
1129
1129
  // src/commands/connector.ts
1130
+ init_agent();
1130
1131
  import chalk6 from "chalk";
1131
1132
  var PROVIDERS = [
1132
1133
  // Communication
@@ -1233,8 +1234,10 @@ ${filtered.length} providers total. Connect at blink.new/settings?tab=connectors
1233
1234
  if (d.metadata?.email) console.log(chalk6.dim(` Email: ${d.metadata.email}`));
1234
1235
  if (d.metadata?.name) console.log(chalk6.dim(` Name: ${d.metadata.name}`));
1235
1236
  } else {
1237
+ const agentUrl = process.env.BLINK_WORKSPACE_SLUG && process.env.BLINK_AGENT_ID ? `https://blink.new/${process.env.BLINK_WORKSPACE_SLUG}/claw/${process.env.BLINK_AGENT_ID}` : "https://blink.new/settings?tab=connectors";
1236
1238
  console.log(chalk6.red("\u2717 Not connected"));
1237
- console.log(chalk6.dim(` Connect at blink.new/settings?tab=connectors`));
1239
+ console.log(chalk6.dim(` Fix: blink connector link ${provider}`));
1240
+ console.log(chalk6.dim(` Or visit: ${agentUrl}`));
1238
1241
  }
1239
1242
  } else {
1240
1243
  const result = await withSpinner(
@@ -1257,6 +1260,40 @@ ${filtered.length} providers total. Connect at blink.new/settings?tab=connectors
1257
1260
  ${connected.length} provider(s) connected`));
1258
1261
  }
1259
1262
  });
1263
+ connector.command("link <provider>").description("Auto-link a workspace connector to the current agent").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1264
+ Links the workspace's connected account for <provider> to this agent automatically.
1265
+ The workspace must already have the account connected at blink.new/settings?tab=connectors.
1266
+
1267
+ This is the self-service fix when an agent gets "not linked" errors \u2014 run this once
1268
+ and the connector will be available for all subsequent commands.
1269
+
1270
+ Examples:
1271
+ $ blink connector link linkedin Link LinkedIn to this agent
1272
+ $ blink connector link notion Link Notion to this agent
1273
+ $ blink connector link slack Link Slack to this agent
1274
+ $ blink connector link linkedin --json Machine-readable output
1275
+ `).action(async (provider, opts) => {
1276
+ requireToken();
1277
+ const agentId = requireAgentId(opts.agent);
1278
+ const result = await withSpinner(
1279
+ `Linking ${provider} to agent ${agentId}...`,
1280
+ () => resourcesRequest(`/v1/connectors/${provider}/link`, {
1281
+ method: "POST",
1282
+ headers: { "x-blink-agent-id": agentId },
1283
+ body: {}
1284
+ })
1285
+ );
1286
+ if (!result?.success) {
1287
+ const agentUrl = process.env.BLINK_WORKSPACE_SLUG && process.env.BLINK_AGENT_ID ? `https://blink.new/${process.env.BLINK_WORKSPACE_SLUG}/claw/${process.env.BLINK_AGENT_ID}` : "https://blink.new/settings?tab=connectors";
1288
+ console.error(chalk6.red(`\u2717 ${result?.error ?? `No ${provider} account in workspace`}`));
1289
+ console.error(chalk6.dim(` Connect one first: ${agentUrl}`));
1290
+ process.exit(1);
1291
+ }
1292
+ if (isJsonMode()) return printJson(result.data);
1293
+ const label = result.data?.account_label ?? provider;
1294
+ console.log(chalk6.green(`\u2713 ${provider} linked`) + chalk6.dim(` (${label})`));
1295
+ console.log(chalk6.dim(` Agent ${agentId} can now use blink connector exec ${provider}`));
1296
+ });
1260
1297
  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", `
1261
1298
  <endpoint> is the API path relative to the provider's base URL, OR a GraphQL query string for Linear.
1262
1299
 
@@ -1332,13 +1369,24 @@ Provider base URLs used:
1332
1369
  // src/commands/linkedin.ts
1333
1370
  init_agent();
1334
1371
  import chalk7 from "chalk";
1335
- var NOT_LINKED = "LinkedIn not linked. Link it in the Agent Integrations tab at blink.new/claw";
1372
+ function getAgentPageUrl() {
1373
+ const slug = process.env.BLINK_WORKSPACE_SLUG;
1374
+ const agentId = process.env.BLINK_AGENT_ID;
1375
+ if (slug && agentId) return `https://blink.new/${slug}/claw/${agentId}`;
1376
+ return "https://blink.new/claw";
1377
+ }
1378
+ function notLinkedError() {
1379
+ const url = getAgentPageUrl();
1380
+ return `LinkedIn not linked to this agent.
1381
+ Fix: blink connector link linkedin
1382
+ Or connect manually: ${url}`;
1383
+ }
1336
1384
  async function liExec(method, httpMethod, params, agentId) {
1337
1385
  const result = await resourcesRequest("/v1/connectors/linkedin/execute", {
1338
1386
  body: { method, http_method: httpMethod, params },
1339
1387
  headers: { "x-blink-agent-id": agentId }
1340
1388
  });
1341
- if (!result?.success) throw new Error(result?.error ?? NOT_LINKED);
1389
+ if (!result?.success) throw new Error(result?.error ?? notLinkedError());
1342
1390
  return result.data;
1343
1391
  }
1344
1392
  async function getPersonId(agentId) {
@@ -1348,30 +1396,40 @@ async function getPersonId(agentId) {
1348
1396
  return id;
1349
1397
  }
1350
1398
  function registerLinkedInCommands(program2) {
1351
- const li = program2.command("linkedin").description("LinkedIn connector \u2014 publish posts and manage your profile").addHelpText("after", `
1399
+ const li = program2.command("linkedin").description("LinkedIn connector \u2014 publish posts, comment, react, and manage your profile").addHelpText("after", `
1352
1400
  LinkedIn must be linked to your agent via the Integrations tab at blink.new/claw.
1353
1401
  Agent ID defaults to BLINK_AGENT_ID (automatically set on Claw Fly machines).
1354
1402
 
1355
1403
  What works today (w_member_social scope):
1356
1404
  \u2705 blink linkedin me Show your LinkedIn profile
1357
- \u2705 blink linkedin post "Excited to announce..." Publish a text post
1405
+ \u2705 blink linkedin post "text" Publish a text post
1358
1406
  \u2705 blink linkedin delete <postUrn> Delete one of your posts
1407
+ \u2705 blink linkedin like <postUrn> Like a post
1408
+ \u2705 blink linkedin unlike <postUrn> Unlike a post
1409
+ \u2705 blink linkedin comment <postUrn> "text" Add a comment
1359
1410
 
1360
- Not yet available (requires LinkedIn Partner API approval):
1361
- \u2717 Reading posts, comments, likes (needs r_member_social \u2014 restricted scope)
1362
- \u2717 Adding comments (needs Community Management API)
1411
+ For feed reading, search, and profiles \u2014 use scripts/lk.py (requires cookies):
1412
+ python3 scripts/lk.py feed -n 10 Browse your LinkedIn feed
1413
+ python3 scripts/lk.py search "query" Search people
1414
+ python3 scripts/lk.py profile <id> View a profile
1415
+ python3 scripts/lk.py messages Check messages
1416
+ See SKILL.md for cookie setup instructions.
1417
+
1418
+ Post URNs: use the URN returned by "blink linkedin post --json" or extract from
1419
+ a LinkedIn post URL: https://linkedin.com/feed/update/urn:li:activity:123...
1363
1420
 
1364
1421
  Examples:
1365
1422
  $ blink linkedin me
1366
- $ blink linkedin post "Our product just launched!"
1367
- $ blink linkedin post "Team update" --visibility CONNECTIONS
1368
- $ blink linkedin delete "urn:li:ugcPost:1234567890"
1423
+ $ blink linkedin post "Our product just launched! \u{1F680}"
1424
+ $ blink linkedin like "urn:li:share:1234567890"
1425
+ $ blink linkedin comment "urn:li:share:1234567890" "Great post!"
1426
+ $ blink linkedin delete "urn:li:share:1234567890"
1369
1427
  `);
1370
1428
  li.command("me").description("Show your LinkedIn profile (name, ID, email)").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1371
1429
  Examples:
1372
1430
  $ blink linkedin me
1373
1431
  $ blink linkedin me --json
1374
- $ blink linkedin me --agent clw_xxx
1432
+ $ PERSON_ID=$(blink linkedin me --json | jq -r .sub)
1375
1433
  `).action(async (opts) => {
1376
1434
  requireToken();
1377
1435
  const agentId = requireAgentId(opts.agent);
@@ -1388,10 +1446,12 @@ Examples:
1388
1446
  if (data?.email) console.log(` ${chalk7.dim("Email:")} ${data.email}`);
1389
1447
  });
1390
1448
  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", `
1449
+ Returns the post URN which you can pass to like/comment/delete.
1450
+
1391
1451
  Examples:
1392
1452
  $ blink linkedin post "Excited to share our latest update!"
1393
- $ blink linkedin post "Internal team update" --visibility CONNECTIONS
1394
- $ blink linkedin post "Hello LinkedIn" --json
1453
+ $ blink linkedin post "Internal update" --visibility CONNECTIONS
1454
+ $ POST_URN=$(blink linkedin post "Hello LinkedIn" --json | jq -r .id)
1395
1455
  `).action(async (text, opts) => {
1396
1456
  requireToken();
1397
1457
  const agentId = requireAgentId(opts.agent);
@@ -1423,46 +1483,88 @@ Examples:
1423
1483
  console.log(chalk7.green("\u2713 Post published"));
1424
1484
  if (data?.id) console.log(chalk7.dim(` URN: ${data.id}`));
1425
1485
  });
1426
- li.command("upload-media <media-url>").description("Upload an image or video to LinkedIn storage, returns asset URN for use in posts").option("--type <type>", "Media type: image | video (default: image)", "image").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1427
- Returns an asset URN to use when composing a post with media via blink connector exec.
1428
-
1486
+ li.command("delete <postUrn>").description("Delete one of your LinkedIn posts").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1429
1487
  Examples:
1430
- $ blink linkedin upload-media https://example.com/photo.jpg
1431
- $ blink linkedin upload-media https://example.com/demo.mp4 --type video
1432
- $ ASSET_URN=$(blink linkedin upload-media https://example.com/photo.jpg --json | python3 -c "import json,sys; print(json.load(sys.stdin)['asset_urn'])")
1433
- `).action(async (mediaUrl, opts) => {
1488
+ $ blink linkedin delete "urn:li:share:1234567890"
1489
+ `).action(async (postUrn, opts) => {
1434
1490
  requireToken();
1435
1491
  const agentId = requireAgentId(opts.agent);
1436
- const result = await withSpinner(
1437
- `Uploading ${opts.type}...`,
1438
- () => resourcesRequest("/v1/connectors/linkedin/upload-media", {
1439
- body: { media_url: mediaUrl, media_type: opts.type },
1440
- headers: { "x-blink-agent-id": agentId }
1441
- })
1492
+ const encoded = encodeURIComponent(postUrn);
1493
+ await withSpinner(
1494
+ "Deleting post...",
1495
+ () => liExec(`ugcPosts/${encoded}`, "DELETE", {}, agentId)
1442
1496
  );
1443
- if (isJsonMode()) return printJson(result?.data ?? result);
1444
- const assetUrn = result?.data?.asset_urn;
1445
- if (assetUrn) {
1446
- console.log(chalk7.green("\u2713 Upload complete"));
1447
- console.log(chalk7.dim(` Asset URN: ${assetUrn}`));
1448
- }
1497
+ if (isJsonMode()) return printJson({ deleted: true, urn: postUrn });
1498
+ console.log(chalk7.green("\u2713 Post deleted"));
1449
1499
  });
1450
- li.command("delete <postUrn>").description("Delete one of your LinkedIn posts").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1451
- <postUrn> is the LinkedIn post URN returned when the post was created.
1452
- e.g. urn:li:ugcPost:1234567890
1500
+ li.command("like <postUrn>").description("Like a LinkedIn post").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1501
+ <postUrn> accepts urn:li:share:, urn:li:ugcPost:, or urn:li:activity: URNs.
1502
+ LinkedIn feed URLs contain the activity URN: linkedin.com/feed/update/urn:li:activity:123
1453
1503
 
1454
1504
  Examples:
1455
- $ blink linkedin delete "urn:li:ugcPost:1234567890"
1505
+ $ blink linkedin like "urn:li:share:1234567890"
1506
+ $ blink linkedin like "urn:li:activity:1234567890"
1456
1507
  `).action(async (postUrn, opts) => {
1457
1508
  requireToken();
1458
1509
  const agentId = requireAgentId(opts.agent);
1510
+ const personId = await withSpinner(
1511
+ "Resolving LinkedIn identity...",
1512
+ () => getPersonId(agentId)
1513
+ );
1459
1514
  const encoded = encodeURIComponent(postUrn);
1515
+ const data = await withSpinner(
1516
+ "Liking post...",
1517
+ () => liExec(`v2/socialActions/${encoded}/likes`, "POST", {
1518
+ actor: `urn:li:person:${personId}`
1519
+ }, agentId)
1520
+ );
1521
+ if (isJsonMode()) return printJson(data);
1522
+ console.log(chalk7.green("\u2713 Post liked"));
1523
+ if (data?.["$URN"]) console.log(chalk7.dim(` Like URN: ${data["$URN"]}`));
1524
+ });
1525
+ li.command("unlike <postUrn>").description("Unlike a LinkedIn post").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1526
+ Examples:
1527
+ $ blink linkedin unlike "urn:li:share:1234567890"
1528
+ `).action(async (postUrn, opts) => {
1529
+ requireToken();
1530
+ const agentId = requireAgentId(opts.agent);
1531
+ const personId = await withSpinner(
1532
+ "Resolving LinkedIn identity...",
1533
+ () => getPersonId(agentId)
1534
+ );
1535
+ const encodedPost = encodeURIComponent(postUrn);
1536
+ const encodedPerson = encodeURIComponent(`urn:li:person:${personId}`);
1460
1537
  await withSpinner(
1461
- "Deleting post...",
1462
- () => liExec(`ugcPosts/${encoded}`, "DELETE", {}, agentId)
1538
+ "Unliking post...",
1539
+ () => liExec(`v2/socialActions/${encodedPost}/likes/${encodedPerson}`, "DELETE", {}, agentId)
1463
1540
  );
1464
- if (isJsonMode()) return printJson({ deleted: true, urn: postUrn });
1465
- console.log(chalk7.green("\u2713 Post deleted"));
1541
+ if (isJsonMode()) return printJson({ unliked: true });
1542
+ console.log(chalk7.green("\u2713 Post unliked"));
1543
+ });
1544
+ 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", `
1545
+ <postUrn> accepts urn:li:share:, urn:li:ugcPost:, or urn:li:activity: URNs.
1546
+
1547
+ Examples:
1548
+ $ blink linkedin comment "urn:li:share:1234567890" "Great post!"
1549
+ $ blink linkedin comment "urn:li:activity:1234567890" "Thanks for sharing"
1550
+ `).action(async (postUrn, text, opts) => {
1551
+ requireToken();
1552
+ const agentId = requireAgentId(opts.agent);
1553
+ const personId = await withSpinner(
1554
+ "Resolving LinkedIn identity...",
1555
+ () => getPersonId(agentId)
1556
+ );
1557
+ const encoded = encodeURIComponent(postUrn);
1558
+ const data = await withSpinner(
1559
+ "Adding comment...",
1560
+ () => liExec(`v2/socialActions/${encoded}/comments`, "POST", {
1561
+ actor: `urn:li:person:${personId}`,
1562
+ message: { text }
1563
+ }, agentId)
1564
+ );
1565
+ if (isJsonMode()) return printJson(data);
1566
+ console.log(chalk7.green("\u2713 Comment added"));
1567
+ if (data?.id) console.log(chalk7.dim(` Comment ID: ${data.id}`));
1466
1568
  });
1467
1569
  }
1468
1570
 
@@ -1588,7 +1690,8 @@ Examples:
1588
1690
  The number is permanently returned to the carrier pool. This action cannot be undone.
1589
1691
  `).action(async (id, opts) => {
1590
1692
  requireToken();
1591
- if (!opts.yes && !isJsonMode()) {
1693
+ const skipConfirm = process.argv.includes("--yes") || process.argv.includes("-y") || isJsonMode();
1694
+ if (!skipConfirm) {
1592
1695
  const { confirm } = await import("@clack/prompts");
1593
1696
  const yes = await confirm({ message: `Release ${id}? This cannot be undone.` });
1594
1697
  if (!yes) {
@@ -1996,6 +2099,41 @@ Examples:
1996
2099
  console.log(chalk11.bold("Machine ") + (a.machine_size ?? "-"));
1997
2100
  if (a.fly_app_name) console.log(chalk11.bold("Fly App ") + a.fly_app_name);
1998
2101
  });
2102
+ agent.command("url [agent_id]").description("Print the blink.new page URL for this agent (for sharing / setup links)").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID or BLINK_ACTIVE_AGENT)").addHelpText("after", `
2103
+ Returns the direct URL to this agent's page on blink.new.
2104
+ On Claw machines, uses BLINK_WORKSPACE_SLUG + BLINK_AGENT_ID env vars (no API call).
2105
+ Otherwise fetches workspace slug from the API.
2106
+
2107
+ Examples:
2108
+ $ blink agent url
2109
+ https://blink.new/kai/claw/clw_xxx
2110
+
2111
+ $ blink agent url --json
2112
+ {"url":"https://blink.new/kai/claw/clw_xxx","agent_id":"clw_xxx","workspace_slug":"kai"}
2113
+ `).action(async (agentIdArg, opts) => {
2114
+ requireToken();
2115
+ const agentId = requireAgentId(opts.agent ?? agentIdArg);
2116
+ const envSlug = process.env.BLINK_WORKSPACE_SLUG;
2117
+ if (envSlug) {
2118
+ const url2 = `https://blink.new/${envSlug}/claw/${agentId}`;
2119
+ if (isJsonMode()) return printJson({ url: url2, agent_id: agentId, workspace_slug: envSlug });
2120
+ console.log(url2);
2121
+ return;
2122
+ }
2123
+ const result = await withSpinner(
2124
+ "Fetching agent details...",
2125
+ () => appRequest(`/api/claw/agents/${agentId}`)
2126
+ );
2127
+ const a = result?.agent ?? result;
2128
+ const workspaceSlug = a?.workspace_slug ?? a?.workspace_id;
2129
+ if (!workspaceSlug) {
2130
+ console.error(chalk11.red("Could not determine workspace slug"));
2131
+ process.exit(1);
2132
+ }
2133
+ const url = `https://blink.new/${workspaceSlug}/claw/${agentId}`;
2134
+ if (isJsonMode()) return printJson({ url, agent_id: agentId, workspace_slug: workspaceSlug });
2135
+ console.log(url);
2136
+ });
1999
2137
  }
2000
2138
 
2001
2139
  // src/commands/secrets.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Blink platform CLI — deploy apps, manage databases, generate AI content",
5
5
  "bin": {
6
6
  "blink": "dist/cli.js"
@@ -86,4 +86,48 @@ Examples:
86
86
  console.log(chalk.bold('Machine ') + (a.machine_size ?? '-'))
87
87
  if (a.fly_app_name) console.log(chalk.bold('Fly App ') + a.fly_app_name)
88
88
  })
89
+
90
+ // blink agent url
91
+ agent.command('url [agent_id]')
92
+ .description('Print the blink.new page URL for this agent (for sharing / setup links)')
93
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID or BLINK_ACTIVE_AGENT)')
94
+ .addHelpText('after', `
95
+ Returns the direct URL to this agent's page on blink.new.
96
+ On Claw machines, uses BLINK_WORKSPACE_SLUG + BLINK_AGENT_ID env vars (no API call).
97
+ Otherwise fetches workspace slug from the API.
98
+
99
+ Examples:
100
+ $ blink agent url
101
+ https://blink.new/kai/claw/clw_xxx
102
+
103
+ $ blink agent url --json
104
+ {"url":"https://blink.new/kai/claw/clw_xxx","agent_id":"clw_xxx","workspace_slug":"kai"}
105
+ `)
106
+ .action(async (agentIdArg: string | undefined, opts) => {
107
+ requireToken()
108
+ const agentId = requireAgentId(opts.agent ?? agentIdArg)
109
+
110
+ // Fast path: env vars already set on Claw machines (no API call needed)
111
+ const envSlug = process.env.BLINK_WORKSPACE_SLUG
112
+ if (envSlug) {
113
+ const url = `https://blink.new/${envSlug}/claw/${agentId}`
114
+ if (isJsonMode()) return printJson({ url, agent_id: agentId, workspace_slug: envSlug })
115
+ console.log(url)
116
+ return
117
+ }
118
+
119
+ // Fallback: fetch workspace slug from API
120
+ const result = await withSpinner('Fetching agent details...', () =>
121
+ appRequest(`/api/claw/agents/${agentId}`)
122
+ )
123
+ const a = result?.agent ?? result
124
+ const workspaceSlug = a?.workspace_slug ?? a?.workspace_id
125
+ if (!workspaceSlug) {
126
+ console.error(chalk.red('Could not determine workspace slug'))
127
+ process.exit(1)
128
+ }
129
+ const url = `https://blink.new/${workspaceSlug}/claw/${agentId}`
130
+ if (isJsonMode()) return printJson({ url, agent_id: agentId, workspace_slug: workspaceSlug })
131
+ console.log(url)
132
+ })
89
133
  }
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander'
2
2
  import { resourcesRequest } from '../lib/api-resources.js'
3
3
  import { requireToken } from '../lib/auth.js'
4
+ import { requireAgentId } from '../lib/agent.js'
4
5
  import { printJson, isJsonMode, withSpinner, createTable } from '../lib/output.js'
5
6
  import chalk from 'chalk'
6
7
 
@@ -124,8 +125,12 @@ Use --account <id> if you have multiple linked accounts for the same provider.
124
125
  if (d.metadata?.email) console.log(chalk.dim(` Email: ${d.metadata.email}`))
125
126
  if (d.metadata?.name) console.log(chalk.dim(` Name: ${d.metadata.name}`))
126
127
  } else {
128
+ const agentUrl = process.env.BLINK_WORKSPACE_SLUG && process.env.BLINK_AGENT_ID
129
+ ? `https://blink.new/${process.env.BLINK_WORKSPACE_SLUG}/claw/${process.env.BLINK_AGENT_ID}`
130
+ : 'https://blink.new/settings?tab=connectors'
127
131
  console.log(chalk.red('✗ Not connected'))
128
- console.log(chalk.dim(` Connect at blink.new/settings?tab=connectors`))
132
+ console.log(chalk.dim(` Fix: blink connector link ${provider}`))
133
+ console.log(chalk.dim(` Or visit: ${agentUrl}`))
129
134
  }
130
135
  } else {
131
136
  // List all connected providers
@@ -149,6 +154,47 @@ Use --account <id> if you have multiple linked accounts for the same provider.
149
154
  }
150
155
  })
151
156
 
157
+ // blink connector link <provider>
158
+ connector.command('link <provider>')
159
+ .description('Auto-link a workspace connector to the current agent')
160
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
161
+ .addHelpText('after', `
162
+ Links the workspace's connected account for <provider> to this agent automatically.
163
+ The workspace must already have the account connected at blink.new/settings?tab=connectors.
164
+
165
+ This is the self-service fix when an agent gets "not linked" errors — run this once
166
+ and the connector will be available for all subsequent commands.
167
+
168
+ Examples:
169
+ $ blink connector link linkedin Link LinkedIn to this agent
170
+ $ blink connector link notion Link Notion to this agent
171
+ $ blink connector link slack Link Slack to this agent
172
+ $ blink connector link linkedin --json Machine-readable output
173
+ `)
174
+ .action(async (provider: string, opts) => {
175
+ requireToken()
176
+ const agentId = requireAgentId(opts.agent)
177
+ const result = await withSpinner(`Linking ${provider} to agent ${agentId}...`, () =>
178
+ resourcesRequest(`/v1/connectors/${provider}/link`, {
179
+ method: 'POST',
180
+ headers: { 'x-blink-agent-id': agentId },
181
+ body: {},
182
+ })
183
+ )
184
+ if (!result?.success) {
185
+ const agentUrl = process.env.BLINK_WORKSPACE_SLUG && process.env.BLINK_AGENT_ID
186
+ ? `https://blink.new/${process.env.BLINK_WORKSPACE_SLUG}/claw/${process.env.BLINK_AGENT_ID}`
187
+ : 'https://blink.new/settings?tab=connectors'
188
+ console.error(chalk.red(`✗ ${result?.error ?? `No ${provider} account in workspace`}`))
189
+ console.error(chalk.dim(` Connect one first: ${agentUrl}`))
190
+ process.exit(1)
191
+ }
192
+ if (isJsonMode()) return printJson(result.data)
193
+ const label = result.data?.account_label ?? provider
194
+ console.log(chalk.green(`✓ ${provider} linked`) + chalk.dim(` (${label})`))
195
+ console.log(chalk.dim(` Agent ${agentId} can now use blink connector exec ${provider}`))
196
+ })
197
+
152
198
  // blink connector exec <provider> <endpoint> [method-or-params] [params]
153
199
  // Supports both patterns:
154
200
  // blink connector exec github /user/repos GET
@@ -5,7 +5,19 @@ import { printJson, isJsonMode, withSpinner } from '../lib/output.js'
5
5
  import { resourcesRequest } from '../lib/api-resources.js'
6
6
  import chalk from 'chalk'
7
7
 
8
- const NOT_LINKED = 'LinkedIn not linked. Link it in the Agent Integrations tab at blink.new/claw'
8
+ // Build agent page URL from env vars (set at machine creation via BLINK_WORKSPACE_SLUG).
9
+ // Falls back to generic URL if env vars aren't set (e.g. local dev).
10
+ function getAgentPageUrl(): string {
11
+ const slug = process.env.BLINK_WORKSPACE_SLUG
12
+ const agentId = process.env.BLINK_AGENT_ID
13
+ if (slug && agentId) return `https://blink.new/${slug}/claw/${agentId}`
14
+ return 'https://blink.new/claw'
15
+ }
16
+
17
+ function notLinkedError(): string {
18
+ const url = getAgentPageUrl()
19
+ return `LinkedIn not linked to this agent.\n Fix: blink connector link linkedin\n Or connect manually: ${url}`
20
+ }
9
21
 
10
22
  async function liExec(
11
23
  method: string,
@@ -17,7 +29,7 @@ async function liExec(
17
29
  body: { method, http_method: httpMethod, params },
18
30
  headers: { 'x-blink-agent-id': agentId },
19
31
  })
20
- if (!result?.success) throw new Error(result?.error ?? NOT_LINKED)
32
+ if (!result?.success) throw new Error(result?.error ?? notLinkedError())
21
33
  return result.data
22
34
  }
23
35
 
@@ -30,25 +42,35 @@ async function getPersonId(agentId: string): Promise<string> {
30
42
 
31
43
  export function registerLinkedInCommands(program: Command) {
32
44
  const li = program.command('linkedin')
33
- .description('LinkedIn connector — publish posts and manage your profile')
45
+ .description('LinkedIn connector — publish posts, comment, react, and manage your profile')
34
46
  .addHelpText('after', `
35
47
  LinkedIn must be linked to your agent via the Integrations tab at blink.new/claw.
36
48
  Agent ID defaults to BLINK_AGENT_ID (automatically set on Claw Fly machines).
37
49
 
38
50
  What works today (w_member_social scope):
39
51
  ✅ blink linkedin me Show your LinkedIn profile
40
- ✅ blink linkedin post "Excited to announce..." Publish a text post
52
+ ✅ blink linkedin post "text" Publish a text post
41
53
  ✅ blink linkedin delete <postUrn> Delete one of your posts
54
+ ✅ blink linkedin like <postUrn> Like a post
55
+ ✅ blink linkedin unlike <postUrn> Unlike a post
56
+ ✅ blink linkedin comment <postUrn> "text" Add a comment
57
+
58
+ For feed reading, search, and profiles — use scripts/lk.py (requires cookies):
59
+ python3 scripts/lk.py feed -n 10 Browse your LinkedIn feed
60
+ python3 scripts/lk.py search "query" Search people
61
+ python3 scripts/lk.py profile <id> View a profile
62
+ python3 scripts/lk.py messages Check messages
63
+ See SKILL.md for cookie setup instructions.
42
64
 
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)
65
+ Post URNs: use the URN returned by "blink linkedin post --json" or extract from
66
+ a LinkedIn post URL: https://linkedin.com/feed/update/urn:li:activity:123...
46
67
 
47
68
  Examples:
48
69
  $ 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"
70
+ $ blink linkedin post "Our product just launched! 🚀"
71
+ $ blink linkedin like "urn:li:share:1234567890"
72
+ $ blink linkedin comment "urn:li:share:1234567890" "Great post!"
73
+ $ blink linkedin delete "urn:li:share:1234567890"
52
74
  `)
53
75
 
54
76
  // blink linkedin me
@@ -59,7 +81,7 @@ Examples:
59
81
  Examples:
60
82
  $ blink linkedin me
61
83
  $ blink linkedin me --json
62
- $ blink linkedin me --agent clw_xxx
84
+ $ PERSON_ID=$(blink linkedin me --json | jq -r .sub)
63
85
  `)
64
86
  .action(async (opts) => {
65
87
  requireToken()
@@ -82,10 +104,12 @@ Examples:
82
104
  .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
83
105
  .option('--visibility <vis>', 'PUBLIC | CONNECTIONS (default: PUBLIC)', 'PUBLIC')
84
106
  .addHelpText('after', `
107
+ Returns the post URN which you can pass to like/comment/delete.
108
+
85
109
  Examples:
86
110
  $ 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
111
+ $ blink linkedin post "Internal update" --visibility CONNECTIONS
112
+ $ POST_URN=$(blink linkedin post "Hello LinkedIn" --json | jq -r .id)
89
113
  `)
90
114
  .action(async (text: string, opts) => {
91
115
  requireToken()
@@ -117,55 +141,103 @@ Examples:
117
141
  if (data?.id) console.log(chalk.dim(` URN: ${data.id}`))
118
142
  })
119
143
 
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')
144
+ // blink linkedin delete <postUrn>
145
+ li.command('delete <postUrn>')
146
+ .description('Delete one of your LinkedIn posts')
124
147
  .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
125
148
  .addHelpText('after', `
126
- Returns an asset URN to use when composing a post with media via blink connector exec.
127
-
128
149
  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'])")
150
+ $ blink linkedin delete "urn:li:share:1234567890"
132
151
  `)
133
- .action(async (mediaUrl: string, opts) => {
152
+ .action(async (postUrn: string, opts) => {
134
153
  requireToken()
135
154
  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
- })
155
+ const encoded = encodeURIComponent(postUrn)
156
+ await withSpinner('Deleting post...', () =>
157
+ liExec(`ugcPosts/${encoded}`, 'DELETE', {}, agentId)
141
158
  )
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
- }
159
+ if (isJsonMode()) return printJson({ deleted: true, urn: postUrn })
160
+ console.log(chalk.green('✓ Post deleted'))
148
161
  })
149
162
 
150
- // blink linkedin delete <postUrn>
151
- li.command('delete <postUrn>')
152
- .description('Delete one of your LinkedIn posts')
163
+ // blink linkedin like <postUrn>
164
+ li.command('like <postUrn>')
165
+ .description('Like a LinkedIn post')
153
166
  .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
154
167
  .addHelpText('after', `
155
- <postUrn> is the LinkedIn post URN returned when the post was created.
156
- e.g. urn:li:ugcPost:1234567890
168
+ <postUrn> accepts urn:li:share:, urn:li:ugcPost:, or urn:li:activity: URNs.
169
+ LinkedIn feed URLs contain the activity URN: linkedin.com/feed/update/urn:li:activity:123
157
170
 
158
171
  Examples:
159
- $ blink linkedin delete "urn:li:ugcPost:1234567890"
172
+ $ blink linkedin like "urn:li:share:1234567890"
173
+ $ blink linkedin like "urn:li:activity:1234567890"
160
174
  `)
161
175
  .action(async (postUrn: string, opts) => {
162
176
  requireToken()
163
177
  const agentId = requireAgentId(opts.agent)
178
+ const personId = await withSpinner('Resolving LinkedIn identity...', () =>
179
+ getPersonId(agentId)
180
+ )
164
181
  const encoded = encodeURIComponent(postUrn)
165
- await withSpinner('Deleting post...', () =>
166
- liExec(`ugcPosts/${encoded}`, 'DELETE', {}, agentId)
182
+ const data = await withSpinner('Liking post...', () =>
183
+ liExec(`v2/socialActions/${encoded}/likes`, 'POST', {
184
+ actor: `urn:li:person:${personId}`,
185
+ }, agentId)
167
186
  )
168
- if (isJsonMode()) return printJson({ deleted: true, urn: postUrn })
169
- console.log(chalk.green('✓ Post deleted'))
187
+ if (isJsonMode()) return printJson(data)
188
+ console.log(chalk.green('✓ Post liked'))
189
+ if (data?.['$URN']) console.log(chalk.dim(` Like URN: ${data['$URN']}`))
190
+ })
191
+
192
+ // blink linkedin unlike <postUrn>
193
+ li.command('unlike <postUrn>')
194
+ .description('Unlike a LinkedIn post')
195
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
196
+ .addHelpText('after', `
197
+ Examples:
198
+ $ blink linkedin unlike "urn:li:share:1234567890"
199
+ `)
200
+ .action(async (postUrn: string, opts) => {
201
+ requireToken()
202
+ const agentId = requireAgentId(opts.agent)
203
+ const personId = await withSpinner('Resolving LinkedIn identity...', () =>
204
+ getPersonId(agentId)
205
+ )
206
+ const encodedPost = encodeURIComponent(postUrn)
207
+ const encodedPerson = encodeURIComponent(`urn:li:person:${personId}`)
208
+ await withSpinner('Unliking post...', () =>
209
+ liExec(`v2/socialActions/${encodedPost}/likes/${encodedPerson}`, 'DELETE', {}, agentId)
210
+ )
211
+ if (isJsonMode()) return printJson({ unliked: true })
212
+ console.log(chalk.green('✓ Post unliked'))
213
+ })
214
+
215
+ // blink linkedin comment <postUrn> "text"
216
+ li.command('comment <postUrn> <text>')
217
+ .description('Add a comment to a LinkedIn post')
218
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
219
+ .addHelpText('after', `
220
+ <postUrn> accepts urn:li:share:, urn:li:ugcPost:, or urn:li:activity: URNs.
221
+
222
+ Examples:
223
+ $ blink linkedin comment "urn:li:share:1234567890" "Great post!"
224
+ $ blink linkedin comment "urn:li:activity:1234567890" "Thanks for sharing"
225
+ `)
226
+ .action(async (postUrn: string, text: string, opts) => {
227
+ requireToken()
228
+ const agentId = requireAgentId(opts.agent)
229
+ const personId = await withSpinner('Resolving LinkedIn identity...', () =>
230
+ getPersonId(agentId)
231
+ )
232
+ const encoded = encodeURIComponent(postUrn)
233
+ const data = await withSpinner('Adding comment...', () =>
234
+ liExec(`v2/socialActions/${encoded}/comments`, 'POST', {
235
+ actor: `urn:li:person:${personId}`,
236
+ message: { text },
237
+ }, agentId)
238
+ )
239
+ if (isJsonMode()) return printJson(data)
240
+ console.log(chalk.green('✓ Comment added'))
241
+ if (data?.id) console.log(chalk.dim(` Comment ID: ${data.id}`))
170
242
  })
171
243
  }
@@ -163,7 +163,8 @@ The number is permanently returned to the carrier pool. This action cannot be un
163
163
  `)
164
164
  .action(async (id: string, opts) => {
165
165
  requireToken()
166
- if (!opts.yes && !isJsonMode()) {
166
+ const skipConfirm = process.argv.includes('--yes') || process.argv.includes('-y') || isJsonMode()
167
+ if (!skipConfirm) {
167
168
  const { confirm } = await import('@clack/prompts')
168
169
  const yes = await confirm({ message: `Release ${id}? This cannot be undone.` })
169
170
  if (!yes) { console.log('Cancelled.'); return }