@usesocial/cli 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @usesocial/cli
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#32](https://github.com/usesocial/monorepo/pull/32) [`b2ca106`](https://github.com/usesocial/monorepo/commit/b2ca106f970bfe73d776f3f10225df7c6c7a1b80) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Use cursor pagination for `social linkedin posts` so the CLI matches the LinkedIn
8
+ posts provider contract.
9
+
3
10
  ## 0.7.0
4
11
 
5
12
  ### Minor Changes
package/README.md CHANGED
@@ -115,7 +115,7 @@ profile lookup is needed.
115
115
  | `disconnect x <account>` | Disconnect an X account. | write |
116
116
  | `billing` | Show seats, subscription, and current usage billing. | |
117
117
  | `billing portal` | Open the hosted billing portal and print its URL. | |
118
- | `usage` | Aggregate proxy usage and billing stats. | |
118
+ | `usage` | Aggregate proxy calls and credits. | |
119
119
  | `logs` | Recent proxy calls. | |
120
120
  | `config cache mode` | Read or set the live-read proxy cache mode. | |
121
121
  | `config cache ttl` | Set a live-read proxy cache TTL in seconds. | |
@@ -125,7 +125,7 @@ profile lookup is needed.
125
125
  | Command | Description | |
126
126
  | --- | --- | --- |
127
127
  | `profile [target]` | Fetch a profile; omit target for your own account. | |
128
- | `posts <target>` | Live posts for a person or company; target required. | |
128
+ | `posts <target>` | Live posts for a person or company; target required. Supports `--limit` and `--cursor` from `.meta.cursor`. | |
129
129
  | `comments <target>` | List a post's comments; supports `--limit` and `--offset`. | |
130
130
  | `reactions <target>` | List a post's reactions; supports `--limit` and `--offset`. | |
131
131
  | `connections [target]` | Live connection graph read; omit target for your own account. Supports `--limit` and `--cursor` from `.meta.cursor`. Costs credits. | |
@@ -175,8 +175,9 @@ profile lookup is needed.
175
175
  ## Built for agents
176
176
 
177
177
  - Everything speaks JSON: success on stdout, structured errors on stderr.
178
- - One envelope: lists use `.items[]`, single resources use `.data`, metadata
179
- lives under `.meta`.
178
+ - Platform reads return one envelope: lists use `.items[]`, single resources use
179
+ `.data`, metadata lives under `.meta`. `social account` service commands return
180
+ bare JSON; `account logs` returns `{ items, meta: { cursor } }`.
180
181
  - Schema-backed planning: `social schema --list` is the compact runnable index.
181
182
  - Local SQL is the free surface for your own synced data.
182
183
 
@@ -187,7 +188,7 @@ social linkedin sql "SELECT c.user_display_name, c.user_public_identifier, c.use
187
188
  | jq '.items[]'
188
189
 
189
190
  # Live fan-out: review recent posts, fan out to reactions, rank warm contacts.
190
- social linkedin posts profile_id:<profile-id> --limit 25 --offset 0 > /tmp/recent-posts.json
191
+ social linkedin posts profile_id:<profile-id> --limit 25 > /tmp/recent-posts.json
191
192
  jq -r '.items[].id' /tmp/recent-posts.json \
192
193
  | xargs -I{} social linkedin reactions post_id:{} --limit 100 --offset 0 \
193
194
  | jq -s '[ .[] | .items[] ]
@@ -267,7 +268,9 @@ social linkedin sql "SELECT user_display_name, user_public_identifier FROM li_re
267
268
 
268
269
  # LinkedIn live reads.
269
270
  social linkedin profile
270
- social linkedin posts profile_id:<profile-id> --limit 25 --offset 0
271
+ PAGE1=$(social linkedin posts profile_id:<profile-id> --limit 25)
272
+ NEXT=$(echo "$PAGE1" | jq -r '.meta.cursor // empty')
273
+ [ -n "$NEXT" ] && social linkedin posts profile_id:<profile-id> --limit 25 --cursor "$NEXT"
271
274
  social linkedin reactions post_id:<post-id> --limit 100 --offset 0
272
275
 
273
276
  # X local mirror.
@@ -290,7 +293,8 @@ echo "Loved your launch notes." | social linkedin requests send profile_id:<prof
290
293
  social account billing
291
294
  social account billing portal
292
295
  social account usage
293
- social account logs --limit 20 --from 2026-05-01T00:00:00Z
296
+ social account logs --limit 20 --from 2026-05-01T00:00:00Z \
297
+ | jq -r '.items[] | [.createdAt, .platform, .method, .path, .responseStatus, .cacheStatus, .credits] | @tsv'
294
298
 
295
299
  # Command schema.
296
300
  social schema
package/dist/index.mjs CHANGED
@@ -307,14 +307,67 @@ const referencedTablesForRead = (sql, platform) => {
307
307
  };
308
308
  const collectionForReadTable = (table) => table === "x_raw_messages" ? "x_messages" : table;
309
309
  const collectionShortName = (collection) => collection.replace(/^(x|li)_/, "");
310
+ const tableFreshnessPolicies = {
311
+ x_raw_messages: {
312
+ kind: "collection",
313
+ collection: "x_messages"
314
+ },
315
+ li_conversations: {
316
+ kind: "collection",
317
+ collection: "li_conversations",
318
+ commandCollection: "li_messages"
319
+ },
320
+ li_profiles: {
321
+ kind: "platform",
322
+ platform: "linkedin"
323
+ },
324
+ x_profiles: {
325
+ kind: "platform",
326
+ platform: "x"
327
+ }
328
+ };
329
+ const freshnessPolicyForTable = (table) => tableFreshnessPolicies[table] ?? {
330
+ kind: "collection",
331
+ collection: collectionForReadTable(table)
332
+ };
333
+ const platformCollectionPrefix = (platform) => platform === "linkedin" ? "li_" : "x_";
334
+ const syncCommandForPolicy = (platform, policy) => {
335
+ if (policy.kind === "platform") return `social ${policy.platform} sync`;
336
+ return `social ${platform} sync ${collectionShortName(policy.commandCollection ?? policy.collection)}`;
337
+ };
338
+ const missingNameForPolicy = (table, policy) => policy.kind === "platform" ? table : policy.collection;
310
339
  const formatList = (items) => {
311
340
  if (items.length <= 1) return items[0] ?? "";
312
341
  if (items.length === 2) return `${items[0]} and ${items[1]}`;
313
342
  return `${items.slice(0, -1).join(", ")}, and ${items.at(-1)}`;
314
343
  };
315
- const neverSyncedMessage = (platform, collections) => {
316
- const commands = collections.map((collection) => `\`social ${platform} sync ${collectionShortName(collection)}\``);
317
- return `No synced ${formatList(collections)} yet — run ${formatList(commands)} first.`;
344
+ const collectionLastSynced = (db, collection) => {
345
+ return numberOrNull(db.prepare("SELECT last_synced FROM sync_state WHERE collection = :collection").get({ collection })?.last_synced);
346
+ };
347
+ const platformLastSynced = (db, platform) => {
348
+ return numberOrNull(db.prepare("SELECT MAX(last_synced) AS last_synced FROM sync_state WHERE collection GLOB :glob").get({ glob: `${platformCollectionPrefix(platform)}*` })?.last_synced);
349
+ };
350
+ const lastSyncedForPolicy = (db, policy) => policy.kind === "platform" ? platformLastSynced(db, policy.platform) : collectionLastSynced(db, policy.collection);
351
+ const missingFreshnessForTables = (tables) => {
352
+ const missing = [];
353
+ const seen = /* @__PURE__ */ new Set();
354
+ for (const table of tables) {
355
+ if (table.lastSynced != null) continue;
356
+ const key = `${table.missingName}\0${table.command}`;
357
+ if (!seen.has(key)) {
358
+ seen.add(key);
359
+ missing.push({
360
+ name: table.missingName,
361
+ command: table.command
362
+ });
363
+ }
364
+ }
365
+ return missing;
366
+ };
367
+ const neverSyncedMessage = (missing) => {
368
+ const names = missing.map((item) => item.name);
369
+ const commands = missing.map((item) => `\`${item.command}\``);
370
+ return `No synced ${formatList(names)} yet — run ${formatList(commands)} first.`;
318
371
  };
319
372
  const assertKnownProfileTable = (table) => {
320
373
  assertKnownTable(table);
@@ -432,15 +485,16 @@ const createCache = (path, db) => ({
432
485
  read: (sql, opts) => {
433
486
  assertPlatformQuery(sql, opts.platform);
434
487
  const tables = referencedTablesForRead(sql, opts.platform).map((name) => {
435
- const collection = collectionForReadTable(name);
488
+ const policy = freshnessPolicyForTable(name);
436
489
  return {
437
490
  name,
438
- collection,
439
- lastSynced: numberOrNull(db.prepare("SELECT last_synced FROM sync_state WHERE collection = :collection").get({ collection })?.last_synced)
491
+ missingName: missingNameForPolicy(name, policy),
492
+ command: syncCommandForPolicy(opts.platform, policy),
493
+ lastSynced: lastSyncedForPolicy(db, policy)
440
494
  };
441
495
  });
442
- const missingCollections = [...new Set(tables.filter((table) => table.lastSynced == null).map((table) => table.collection))];
443
- if (missingCollections.length > 0) throw new NeverSyncedError(missingCollections, neverSyncedMessage(opts.platform, missingCollections));
496
+ const missingFreshness = missingFreshnessForTables(tables);
497
+ if (missingFreshness.length > 0) throw new NeverSyncedError(missingFreshness.map((item) => item.name), neverSyncedMessage(missingFreshness));
444
498
  return {
445
499
  rows: db.prepare(sql).all(),
446
500
  tables: tables.map(({ name, lastSynced }) => ({
@@ -451,20 +505,23 @@ const createCache = (path, db) => ({
451
505
  },
452
506
  describe: (opts) => {
453
507
  const allowedTables = opts?.platform ? tablesForPlatform(opts.platform) : void 0;
508
+ const tables = db.prepare("SELECT name, type, sql FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name").all().filter((table) => String(table.name) !== "x_raw_messages").filter((table) => allowedTables === void 0 || allowedTables.has(String(table.name)));
509
+ const lastSyncedForTable = (name) => {
510
+ if (name === "sync_state") return null;
511
+ return lastSyncedForPolicy(db, freshnessPolicyForTable(name));
512
+ };
454
513
  return {
455
514
  path,
456
- tables: db.prepare("SELECT name, type, sql FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name").all().filter((table) => String(table.name) !== "x_raw_messages").filter((table) => allowedTables === void 0 || allowedTables.has(String(table.name))).map((table) => {
515
+ tables: tables.map((table) => {
457
516
  const name = String(table.name);
458
517
  const type = table.type === "view" ? "view" : "table";
459
518
  const rowCount = db.prepare(`SELECT count(*) AS rows FROM ${quoteIdentifier(name)}`).get();
460
- const collection = collectionForReadTable(name);
461
- const syncState = name === "sync_state" ? void 0 : db.prepare("SELECT last_synced FROM sync_state WHERE collection = :collection").get({ collection });
462
519
  return {
463
520
  name,
464
521
  type,
465
522
  sql: String(table.sql),
466
523
  rows: Number(rowCount?.rows ?? 0),
467
- last_synced: numberOrNull(syncState?.last_synced)
524
+ last_synced: lastSyncedForTable(name)
468
525
  };
469
526
  })
470
527
  };
@@ -490,13 +547,6 @@ const linkedinRelationsCursorPagination = {
490
547
  cursorPath: "next_cursor",
491
548
  itemsPath: "data"
492
549
  };
493
- const linkedinOffsetPagination = {
494
- style: "offset",
495
- limitParam: "limit",
496
- offsetParam: "offset",
497
- totalCountPath: "total_count",
498
- itemsPath: "data"
499
- };
500
550
  const asRecord$1 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
501
551
  const read$1 = (value) => value === null || value === void 0 ? null : value;
502
552
  const readReaction = (value) => typeof value === "string" ? value : null;
@@ -723,7 +773,7 @@ const LINKEDIN_COLLECTIONS = [
723
773
  platform: "linkedin",
724
774
  requests: [{ path: "/users/:identifier/posts" }],
725
775
  ownIdToken: ":identifier",
726
- pagination: linkedinOffsetPagination,
776
+ pagination: linkedinCursorPagination,
727
777
  pageSize: 50,
728
778
  sinceField: "created_at",
729
779
  lift: liftPost
@@ -8756,6 +8806,7 @@ const stepPage = (state, page, opts) => {
8756
8806
  const newestId = state.firstPage && page.items[0] ? String(page.items[0].id) : state.newestId;
8757
8807
  const rows = [];
8758
8808
  const ids = [];
8809
+ let crossedSince = false;
8759
8810
  for (const item of page.items) {
8760
8811
  const id = String(item.id);
8761
8812
  if (state.checkpoint !== null && id === state.checkpoint) return {
@@ -8771,17 +8822,7 @@ const stepPage = (state, page, opts) => {
8771
8822
  };
8772
8823
  if (opts.since != null && opts.collection.sinceField) {
8773
8824
  const timestamp = liftedTimestamp(opts.collection, item);
8774
- if (timestamp != null && timestamp < opts.since) return {
8775
- rows,
8776
- ids,
8777
- nextState: {
8778
- ...state,
8779
- newestId,
8780
- firstPage: false
8781
- },
8782
- stoppedReason: "since",
8783
- done: true
8784
- };
8825
+ if (timestamp != null && timestamp < opts.since) crossedSince = true;
8785
8826
  }
8786
8827
  const row = opts.collection.lift(item);
8787
8828
  applyBakedQueryOverlay(row, opts.request.bakedQuery);
@@ -8789,6 +8830,17 @@ const stepPage = (state, page, opts) => {
8789
8830
  rows.push(row);
8790
8831
  if (opts.collectIds) ids.push(id);
8791
8832
  }
8833
+ if (crossedSince) return {
8834
+ rows,
8835
+ ids,
8836
+ nextState: {
8837
+ ...state,
8838
+ newestId,
8839
+ firstPage: false
8840
+ },
8841
+ stoppedReason: "since",
8842
+ done: true
8843
+ };
8792
8844
  if (opts.collection.pagination.style === "offset") {
8793
8845
  const nextOffset = state.offset + opts.collection.pageSize;
8794
8846
  const done = page.items.length === 0 || page.totalCount != null && nextOffset >= page.totalCount || page.totalCount == null && page.items.length < opts.collection.pageSize;
@@ -8942,11 +8994,15 @@ const runSync = async (deps, opts) => {
8942
8994
  upserted += result.upserted;
8943
8995
  stoppedReason = mergeStoppedReason(stoppedReason, result.stoppedReason);
8944
8996
  };
8997
+ const writePendingCursor = (pending) => {
8998
+ deps.cache.setCursor(pending.collection, pending.cursor, pending.objectCount, pending.lastSynced);
8999
+ };
8945
9000
  if (collection.parentCollection) {
8946
9001
  const parentCollection = collectionByKey(collection.parentCollection);
8947
9002
  if (!parentCollection) throw new Error(`Missing parent collection ${collection.parentCollection}`);
8948
9003
  const parentIds = [];
8949
9004
  const parentPersistsCheckpoint = parentCollection.requests.length === 1;
9005
+ let parentCheckpoint;
8950
9006
  for (const request of parentCollection.requests) {
8951
9007
  const result = await walkRequest({
8952
9008
  deps,
@@ -8962,17 +9018,18 @@ const runSync = async (deps, opts) => {
8962
9018
  });
8963
9019
  addResult(result);
8964
9020
  parentIds.push(...result.ids);
8965
- if (parentPersistsCheckpoint) pendingCursors.push(singleRequestCheckpoint({
9021
+ if (parentPersistsCheckpoint) parentCheckpoint = singleRequestCheckpoint({
8966
9022
  collection: parentCollection,
8967
9023
  result
8968
- }));
9024
+ });
8969
9025
  }
8970
- if (!parentPersistsCheckpoint) pendingCursors.push({
9026
+ if (!parentPersistsCheckpoint) parentCheckpoint = {
8971
9027
  collection: parentCollection.key,
8972
9028
  cursor: null,
8973
9029
  objectCount: upserted,
8974
9030
  lastSynced: Date.now()
8975
- });
9031
+ };
9032
+ if (parentCheckpoint) writePendingCursor(parentCheckpoint);
8976
9033
  for (const parentId of parentIds) for (const request of collection.requests) addResult(await walkRequest({
8977
9034
  deps,
8978
9035
  collection,
@@ -9019,7 +9076,7 @@ const runSync = async (deps, opts) => {
9019
9076
  lastSynced: Date.now()
9020
9077
  });
9021
9078
  }
9022
- for (const pending of pendingCursors) deps.cache.setCursor(pending.collection, pending.cursor, pending.objectCount, pending.lastSynced);
9079
+ for (const pending of pendingCursors) writePendingCursor(pending);
9023
9080
  return {
9024
9081
  collection: collection.key,
9025
9082
  objectsUpserted: upserted,
@@ -10676,10 +10733,10 @@ const LINKEDIN_OPERATIONS = [
10676
10733
  capability: "read",
10677
10734
  mutates: false,
10678
10735
  pagination: {
10679
- style: "offset",
10736
+ style: "cursor",
10680
10737
  limitParam: "limit",
10681
- offsetParam: "offset",
10682
- totalCountPath: "total_count",
10738
+ cursorParam: "cursor",
10739
+ cursorPath: "next_cursor",
10683
10740
  itemsPath: "data"
10684
10741
  },
10685
10742
  costBPS: 50,
@@ -10688,18 +10745,18 @@ const LINKEDIN_OPERATIONS = [
10688
10745
  },
10689
10746
  parameters: [
10690
10747
  {
10691
- name: "offset",
10748
+ name: "limit",
10692
10749
  in: "query",
10693
10750
  required: false,
10694
10751
  kind: "integer",
10695
- description: "Pagination offset"
10752
+ description: "Rows to return (1-100)"
10696
10753
  },
10697
10754
  {
10698
- name: "limit",
10755
+ name: "cursor",
10699
10756
  in: "query",
10700
10757
  required: false,
10701
- kind: "integer",
10702
- description: "The limit of items to be returned."
10758
+ kind: "string",
10759
+ description: "Pagination cursor"
10703
10760
  },
10704
10761
  {
10705
10762
  name: "identifier",
@@ -13669,8 +13726,8 @@ const UserProfileInput = object$1({
13669
13726
  });
13670
13727
  const UserProfileOutput = record$1(string$1(), unknown$1());
13671
13728
  const UserPostsInput = object$1({
13672
- offset: union$1([string$1(), number$1()]).optional(),
13673
13729
  limit: union$1([string$1(), number$1()]).optional(),
13730
+ cursor: string$1().optional(),
13674
13731
  identifier: string$1()
13675
13732
  });
13676
13733
  const UserPostsOutput = object$1({
@@ -21572,7 +21629,7 @@ const createInstance = (defaults) => {
21572
21629
  const ky = createInstance();
21573
21630
  //#endregion
21574
21631
  //#region package.json
21575
- var version$1 = "0.7.0";
21632
+ var version$1 = "0.7.1";
21576
21633
  //#endregion
21577
21634
  //#region src/lib/env.ts
21578
21635
  const URLWithTrailingSlash = url().transform(ensureTrailingSlash);