@elixpo/lixblogs-cli 1.1.2 → 1.3.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/bin/lixblogs.mjs CHANGED
@@ -34,6 +34,11 @@ import { authProfiles, authUse } from "../src/commands/auth/profiles.js";
34
34
  import { ProfileRegistry, validateProfileId } from "../src/config/ProfileRegistry.js";
35
35
  import { AuthenticatedClient } from "../src/auth/AuthenticatedClient.js";
36
36
  import { BlogClient, BlogApiError } from "../src/api/BlogClient.js";
37
+ import { OrgClient } from "../src/api/OrgClient.js";
38
+ import { CollaborationClient } from "../src/api/CollaborationClient.js";
39
+ import { AnalyticsClient } from "../src/api/AnalyticsClient.js";
40
+ import { EXIT_CODES, errorEnvelope, normalizeCommand } from "../src/cli/contract.js";
41
+ import { colorEnabled, listenForEnter, loginChallenge, successLine } from "../src/cli/ui.js";
37
42
  import {
38
43
  blogCreate,
39
44
  blogDelete,
@@ -44,6 +49,24 @@ import {
44
49
  blogRestore,
45
50
  blogUnpublish,
46
51
  } from "../src/commands/blog/index.js";
52
+ import {
53
+ orgCollections,
54
+ orgGet,
55
+ orgList,
56
+ orgMembers,
57
+ orgTargets,
58
+ } from "../src/commands/org/index.js";
59
+ import {
60
+ collabAccept,
61
+ collabDecline,
62
+ collabInvitations,
63
+ collabInvite,
64
+ collabList,
65
+ collabRemove,
66
+ collabRole,
67
+ } from "../src/commands/collab/index.js";
68
+ import { skillInspect, skillInstall, skillList } from "../src/commands/skill/index.js";
69
+ import { analyticsExport, analyticsQuery } from "../src/commands/analytics/index.js";
47
70
 
48
71
  const OPTIONS = {
49
72
  profile: { type: "string" },
@@ -62,6 +85,12 @@ const OPTIONS = {
62
85
  status: { type: "string" },
63
86
  limit: { type: "string" },
64
87
  cursor: { type: "string" },
88
+ range: { type: "string" },
89
+ from: { type: "string" },
90
+ to: { type: "string" },
91
+ dimension: { type: "string" },
92
+ format: { type: "string" },
93
+ output: { type: "string" },
65
94
  file: { type: "string" },
66
95
  stdin: { type: "boolean", default: false },
67
96
  content: { type: "string" },
@@ -83,12 +112,23 @@ const OPTIONS = {
83
112
  etag: { type: "string" },
84
113
  permanent: { type: "boolean", default: false },
85
114
  "idempotency-key": { type: "string" },
115
+ user: { type: "string" },
116
+ role: { type: "string" },
117
+ "hide-on-profile": { type: "boolean", default: false },
118
+ target: { type: "string" },
119
+ force: { type: "boolean", default: false },
86
120
  help: { type: "boolean", short: "h", default: false },
87
121
  };
88
122
 
89
123
  const HELP_TEXT = `lixblogs — LixBlogs CLI
90
124
 
91
125
  Usage:
126
+ lixblogs login [--profile <name>] [--open]
127
+ lixblogs register [--profile <name>] [--open]
128
+ lixblogs logout [--profile <name>]
129
+ lixblogs whoami [--profile <name>] [--json]
130
+ lixblogs profiles [--json]
131
+ lixblogs use <name> [--json]
92
132
  lixblogs auth login [--profile <name>] [--env <environment>] [--json] [--quiet] [--allow-insecure-fallback]
93
133
  lixblogs auth status [--profile <name>] [--json]
94
134
  lixblogs auth logout [--profile <name>] [--json] [--quiet]
@@ -97,12 +137,31 @@ Usage:
97
137
  lixblogs auth use <name> [--json]
98
138
  lixblogs blog list [--status <status>] [--limit <n>] [--cursor <cursor>] [--json]
99
139
  lixblogs blog get <id> [--json]
140
+ lixblogs blog preview <id> [--json]
100
141
  lixblogs blog create [--file <post.md>|--stdin|--content <markdown>|--editor] [metadata]
101
142
  lixblogs blog edit <id> [--file <post.md>|--stdin|--content <markdown>|--editor] [metadata]
102
- lixblogs blog publish <id> [--dry-run] [--json]
103
- lixblogs blog unpublish <id> [--dry-run] [--json]
143
+ lixblogs blog publish <id> --yes [--dry-run] [--json]
144
+ lixblogs blog unpublish <id> --yes [--dry-run] [--json]
104
145
  lixblogs blog delete <id> --yes [--permanent] [--dry-run] [--json]
105
- lixblogs blog restore <id> [--dry-run] [--json]
146
+ lixblogs blog trash <id> --yes [--dry-run] [--json]
147
+ lixblogs blog restore <id> --yes [--dry-run] [--json]
148
+ lixblogs org list [--json]
149
+ lixblogs org get <id> [--json]
150
+ lixblogs org collections <id> [--json]
151
+ lixblogs org members <id> [--json]
152
+ lixblogs org targets [--json]
153
+ lixblogs collab list <blog-id> [--json]
154
+ lixblogs collab invitations [--json]
155
+ lixblogs collab invite <blog-id> --user <username> --role <viewer|editor|admin> --yes
156
+ lixblogs collab role <blog-id> --user <username-or-id> --role <viewer|editor|admin> --yes
157
+ lixblogs collab remove <blog-id> [--user <username-or-id>] --yes
158
+ lixblogs collab accept <blog-id> --yes [--hide-on-profile]
159
+ lixblogs collab decline <blog-id> --yes
160
+ lixblogs analytics query [--scope personal|org:<id>] [--range 30d] [--dimension overview]
161
+ lixblogs analytics export --output <file> [--format json|csv] [query options]
162
+ lixblogs skill list [--json]
163
+ lixblogs skill inspect <name> [--json]
164
+ lixblogs skill install <name> [--target <directory>] [--dry-run] --yes
106
165
 
107
166
  Global flags:
108
167
  --profile <name> named profile to use (default: "default")
@@ -121,7 +180,7 @@ Global flags:
121
180
  --collection <id> organization collection ID
122
181
  --dry-run validate and show the intended action without writing
123
182
  --permanent permanently delete instead of moving to trash
124
- --open open the complete device verification URL
183
+ --open open the device verification URL immediately
125
184
  --json machine-readable JSON output
126
185
  --quiet suppress non-essential output
127
186
  --yes, -y auto-confirm destructive actions (required for revoke)
@@ -129,8 +188,9 @@ Global flags:
129
188
  non-persistent in-memory store instead of failing
130
189
  --help, -h show this help
131
190
 
132
- Note: interactive confirmation prompting is not implemented yet (CLI-shell/UX
133
- work, a later issue) destructive actions require --yes explicitly, always.
191
+ Machine mode:
192
+ --json --no-input produces stable JSON on stdout, diagnostics on stderr, and
193
+ never prompts. Publishing and destructive state changes require --yes.
134
194
  `;
135
195
 
136
196
  const DEFAULT_SCOPES = [
@@ -169,14 +229,18 @@ function output(opts, data) {
169
229
  }
170
230
  }
171
231
 
172
- function fail(opts, message, exitCode = 1) {
173
- const safeMessage = redactErrorMessage(message);
232
+ function fail(opts, error, exitCode = EXIT_CODES.ERROR) {
233
+ const value = error && typeof error === 'object' ? error : { message: String(error) };
234
+ const safeMessage = redactErrorMessage(value.message);
235
+ const envelope = errorEnvelope({ ...value, message: safeMessage });
174
236
  if (opts.json) {
175
- process.stdout.write(safeJsonStringify({ ok: false, error: safeMessage }) + "\n");
237
+ process.stdout.write(safeJsonStringify(envelope) + "\n");
176
238
  } else if (!opts.quiet) {
177
239
  process.stderr.write(`Error: ${safeMessage}\n`);
240
+ if (value.hint) process.stderr.write(`Hint: ${value.hint}\n`);
241
+ if (value.requestId) process.stderr.write(`Request: ${value.requestId}\n`);
178
242
  }
179
- process.exitCode = exitCode;
243
+ process.exitCode = value.exitCode || exitCode;
180
244
  }
181
245
 
182
246
  /**
@@ -217,35 +281,47 @@ async function runLogin(opts) {
217
281
  const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
218
282
  if (!credentialStore) return;
219
283
 
220
- const result = await authLogin({
221
- provider,
222
- credentialStore,
223
- profileId,
224
- scopes: opts.scope?.length ? opts.scope : DEFAULT_SCOPES,
225
- openBrowser: opts.open ? openBrowser : undefined,
226
- onStatus: (status) => {
227
- if (opts.json) {
228
- output(opts, { event: status.type, ...status });
229
- return;
230
- }
231
- if (opts.quiet) return;
232
- if (status.type === "verification_pending") {
233
- console.log(`To log in, visit: ${status.verificationUriComplete || status.verificationUri}`);
234
- console.log(`Enter code: ${status.userCode}`);
235
- console.log(`(expires in ${status.expiresInSeconds}s)`);
236
- } else if (status.type === "pending") {
237
- console.log("Waiting for approval...");
238
- } else if (status.type === "slow_down") {
239
- console.log("Slowing down polling as requested by the server...");
240
- } else if (status.type === "approved") {
241
- console.log("Login approved.");
242
- } else if (status.type === "denied") {
243
- console.log("Login was denied.");
244
- } else if (status.type === "expired") {
245
- console.log("Device code expired.");
246
- }
247
- },
248
- });
284
+ let stopEnterListener = () => {};
285
+ let result;
286
+ try {
287
+ result = await authLogin({
288
+ provider,
289
+ credentialStore,
290
+ profileId,
291
+ scopes: opts.scope?.length ? opts.scope : DEFAULT_SCOPES,
292
+ openBrowser: opts.open ? openBrowser : undefined,
293
+ onStatus: (status) => {
294
+ if (opts.json) {
295
+ if (status.type !== "pending") output(opts, { event: status.type, ...status });
296
+ return;
297
+ }
298
+ if (opts.quiet) return;
299
+ if (status.type === "verification_pending") {
300
+ const url = status.verificationUriComplete || status.verificationUri;
301
+ const interactive = Boolean(process.stdin.isTTY) && !opts["no-input"];
302
+ process.stdout.write(loginChallenge({
303
+ url,
304
+ code: status.userCode,
305
+ expiresInSeconds: status.expiresInSeconds,
306
+ profile: profileId,
307
+ interactive,
308
+ color: colorEnabled(),
309
+ }));
310
+ if (interactive && !opts.open) {
311
+ stopEnterListener = listenForEnter({ input: process.stdin, open: openBrowser, url });
312
+ }
313
+ } else if (status.type === "approved") {
314
+ console.log(successLine("Access approved by Elixpo Accounts.", colorEnabled()));
315
+ } else if (status.type === "denied") {
316
+ console.log(" Access denied.");
317
+ } else if (status.type === "expired") {
318
+ console.log(" Device code expired.");
319
+ }
320
+ },
321
+ });
322
+ } finally {
323
+ stopEnterListener();
324
+ }
249
325
 
250
326
  if (!result.ok) {
251
327
  return fail(opts, result.reason);
@@ -254,7 +330,7 @@ async function runLogin(opts) {
254
330
  await profileRegistry.setActive(result.profileId);
255
331
  output(opts, { ok: true, profile: result.profileId });
256
332
  if (!opts.json && !opts.quiet) {
257
- console.log(`Logged in as profile "${result.profileId}".`);
333
+ console.log(` Credentials saved to local profile "${result.profileId}".`);
258
334
  }
259
335
  }
260
336
 
@@ -281,6 +357,60 @@ async function runStatus(opts) {
281
357
  }
282
358
  }
283
359
 
360
+ async function authenticatedBlogClient(opts) {
361
+ const config = resolveConfig({ flags: configFlags(opts) });
362
+ const profileRegistry = new ProfileRegistry();
363
+ const profileId = await selectedProfile(config, profileRegistry);
364
+ const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
365
+ if (!credentialStore) return null;
366
+ let provider;
367
+ try { provider = createAuthProvider(config); } catch (error) { fail(opts, error); return null; }
368
+ const http = new AuthenticatedClient({ provider, credentialStore, profileId, apiBaseUrl: config.apiBaseUrl });
369
+ return { client: new BlogClient(http), http, config, credentialStore, profileId };
370
+ }
371
+
372
+ async function runWhoami(opts) {
373
+ const context = await authenticatedBlogClient(opts);
374
+ if (!context) return;
375
+ try {
376
+ const [identity, credentials] = await Promise.all([
377
+ context.client.whoami(),
378
+ context.credentialStore.get(context.profileId),
379
+ ]);
380
+ const result = {
381
+ ok: true,
382
+ profile: context.profileId,
383
+ environment: context.config.environment,
384
+ identity,
385
+ scopes: credentials?.scopes || [],
386
+ expiresAt: credentials?.expiresAt ? new Date(credentials.expiresAt).toISOString() : null,
387
+ expired: credentials ? Date.now() >= credentials.expiresAt : true,
388
+ };
389
+ output(opts, result);
390
+ if (!opts.json && !opts.quiet) {
391
+ console.log(`${identity.displayName || identity.username} (@${identity.username})`);
392
+ console.log(`Profile: ${context.profileId} · ${result.environment}`);
393
+ console.log(`Scopes: ${result.scopes.join(', ') || 'none'}`);
394
+ console.log(`Expires: ${result.expiresAt || 'unknown'}`);
395
+ }
396
+ } catch (error) {
397
+ fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
398
+ }
399
+ }
400
+
401
+ async function runRegister(opts) {
402
+ const config = resolveConfig({ flags: configFlags(opts) });
403
+ const registrationUrl = new URL('/register', config.accountsBaseUrl).toString();
404
+ if (opts['no-input']) {
405
+ output(opts, { ok: true, registrationUrl, next: 'lixblogs login' });
406
+ if (!opts.json && !opts.quiet) console.log(registrationUrl);
407
+ return;
408
+ }
409
+ await openBrowser(registrationUrl);
410
+ if (!opts.quiet) console.log(`Create your account at ${registrationUrl}, then approve the device login.`);
411
+ await runLogin(opts);
412
+ }
413
+
284
414
  async function runLogout(opts) {
285
415
  const config = resolveConfig({ flags: configFlags(opts) });
286
416
  const profileRegistry = new ProfileRegistry();
@@ -370,14 +500,45 @@ async function runUse(opts, args) {
370
500
  const BLOG_COMMANDS = {
371
501
  list: blogList,
372
502
  get: blogGet,
503
+ preview: blogGet,
373
504
  create: blogCreate,
374
505
  edit: blogEdit,
375
506
  publish: blogPublish,
376
507
  unpublish: blogUnpublish,
377
508
  delete: blogDelete,
509
+ trash: blogDelete,
378
510
  restore: blogRestore,
379
511
  };
380
512
 
513
+ const ORG_COMMANDS = {
514
+ list: orgList,
515
+ get: orgGet,
516
+ collections: orgCollections,
517
+ members: orgMembers,
518
+ targets: orgTargets,
519
+ };
520
+
521
+ const COLLAB_COMMANDS = {
522
+ list: collabList,
523
+ invitations: collabInvitations,
524
+ invite: collabInvite,
525
+ role: collabRole,
526
+ remove: collabRemove,
527
+ accept: collabAccept,
528
+ decline: collabDecline,
529
+ };
530
+
531
+ const SKILL_COMMANDS = {
532
+ list: ({ options }) => skillList(options),
533
+ inspect: ({ id }) => skillInspect({ name: id }),
534
+ install: ({ id, options }) => skillInstall({ name: id, options }),
535
+ };
536
+
537
+ const ANALYTICS_COMMANDS = {
538
+ query: analyticsQuery,
539
+ export: analyticsExport,
540
+ };
541
+
381
542
  async function runBlog(opts, args, action) {
382
543
  const config = resolveConfig({ flags: configFlags(opts) });
383
544
  const profileRegistry = new ProfileRegistry();
@@ -420,7 +581,122 @@ async function runBlog(opts, args, action) {
420
581
  process.exitCode = error.status === 412 ? 3 : 1;
421
582
  return;
422
583
  }
423
- return fail(opts, `${error.message}${error.requestId ? ` (request ${error.requestId})` : ''}`, error.status === 412 ? 3 : 1);
584
+ return fail(opts, error, error.status === 412 ? EXIT_CODES.CONFLICT : EXIT_CODES.ERROR);
585
+ }
586
+ }
587
+
588
+ async function runOrg(opts, args, action) {
589
+ const context = await authenticatedBlogClient(opts);
590
+ if (!context) return;
591
+ const client = new OrgClient(context.http);
592
+ try {
593
+ const result = await ORG_COMMANDS[action]({ client, id: args[0], options: opts });
594
+ output(opts, { ok: true, data: result });
595
+ if (opts.json || opts.quiet) return;
596
+ if (action === 'targets') {
597
+ console.log('personal\tPersonal Blog');
598
+ for (const org of result.organizations || []) {
599
+ console.log(`${org.target}\t${org.role}\t${org.name}`);
600
+ for (const collection of org.collections || []) {
601
+ console.log(` collection:${collection.id}\t${collection.name}`);
602
+ }
603
+ }
604
+ return;
605
+ }
606
+ const rows = action === 'list' ? result.data || [] : Array.isArray(result) ? result : [result];
607
+ for (const row of rows) {
608
+ console.log([
609
+ row.id || row.userId || row.orgId,
610
+ row.role,
611
+ row.slug || row.username,
612
+ row.name || row.displayName,
613
+ ].filter(Boolean).join('\t'));
614
+ }
615
+ } catch (error) {
616
+ fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
617
+ }
618
+ }
619
+
620
+ async function runCollab(opts, args, action) {
621
+ const context = await authenticatedBlogClient(opts);
622
+ if (!context) return;
623
+ const client = new CollaborationClient(context.http);
624
+ try {
625
+ const result = await COLLAB_COMMANDS[action]({ client, id: args[0], options: opts });
626
+ output(opts, { ok: true, data: result });
627
+ if (opts.json || opts.quiet) return;
628
+ if (result.dryRun) {
629
+ console.log(`Dry run: ${result.action} validated; no changes sent.`);
630
+ return;
631
+ }
632
+ const rows = action === 'invitations'
633
+ ? result
634
+ : action === 'list'
635
+ ? result.collaborators || []
636
+ : [result];
637
+ for (const row of rows) {
638
+ console.log([
639
+ row.blogId || row.userId,
640
+ row.status,
641
+ row.role,
642
+ row.username || row.title,
643
+ row.notificationState,
644
+ ].filter(Boolean).join('\t'));
645
+ }
646
+ } catch (error) {
647
+ fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
648
+ }
649
+ }
650
+
651
+ async function runSkill(opts, args, action) {
652
+ try {
653
+ const result = await SKILL_COMMANDS[action]({ id: args[0], options: opts });
654
+ output(opts, { ok: true, data: result });
655
+ if (opts.json || opts.quiet) return;
656
+ if (action === 'list') {
657
+ for (const skill of result) console.log(`${skill.name}\tCLI >= ${skill.minimumCliVersion || 'unknown'}\t${skill.description}`);
658
+ } else if (action === 'inspect') {
659
+ process.stdout.write(result.content);
660
+ } else if (result.dryRun) {
661
+ console.log(`Dry run: install ${result.name} to ${result.target}${result.replace ? ' (replace)' : ''}.`);
662
+ } else {
663
+ console.log(`Installed ${result.name} at ${result.target}.`);
664
+ }
665
+ } catch (error) {
666
+ fail(opts, error);
667
+ }
668
+ }
669
+
670
+ async function runAnalytics(opts, _args, action) {
671
+ const context = await authenticatedBlogClient(opts);
672
+ if (!context) return;
673
+ const client = new AnalyticsClient(context.http);
674
+ const normalized = {
675
+ ...opts,
676
+ limit: opts.limit === undefined ? undefined : Number.parseInt(opts.limit, 10),
677
+ };
678
+ try {
679
+ const result = await ANALYTICS_COMMANDS[action]({ client, options: normalized });
680
+ output(opts, { ok: true, data: result });
681
+ if (opts.json || opts.quiet) return;
682
+ if (action === 'export') {
683
+ console.log(`Exported ${result.rows} rows to ${result.output}.`);
684
+ return;
685
+ }
686
+ const payload = result.data;
687
+ console.log(`${payload.scope.label} · ${payload.dimension} · ${payload.range.key}`);
688
+ if (payload.dimension === 'overview') {
689
+ for (const [metric, value] of Object.entries(payload.values.totals)) {
690
+ console.log(`${metric}\t${value}\t${payload.values.changes[metric]}%`);
691
+ }
692
+ } else if (payload.dimension === 'timeline') {
693
+ payload.values.labels.forEach((label, index) => console.log(`${label}\t${payload.values.views[index]}\t${payload.values.reads[index]}`));
694
+ } else {
695
+ for (const row of payload.values) console.log(Object.values(row).join('\t'));
696
+ if (result.meta?.nextCursor) console.log(`Next cursor: ${result.meta.nextCursor}`);
697
+ }
698
+ } catch (error) {
699
+ fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
424
700
  }
425
701
  }
426
702
 
@@ -428,6 +704,7 @@ const ROUTES = {
428
704
  auth: {
429
705
  login: runLogin,
430
706
  status: runStatus,
707
+ whoami: runWhoami,
431
708
  logout: runLogout,
432
709
  revoke: runRevoke,
433
710
  profiles: runProfiles,
@@ -437,6 +714,22 @@ const ROUTES = {
437
714
  action,
438
715
  (opts, args) => runBlog(opts, args, action),
439
716
  ])),
717
+ org: Object.fromEntries(Object.keys(ORG_COMMANDS).map((action) => [
718
+ action,
719
+ (opts, args) => runOrg(opts, args, action),
720
+ ])),
721
+ collab: Object.fromEntries(Object.keys(COLLAB_COMMANDS).map((action) => [
722
+ action,
723
+ (opts, args) => runCollab(opts, args, action),
724
+ ])),
725
+ skill: Object.fromEntries(Object.keys(SKILL_COMMANDS).map((action) => [
726
+ action,
727
+ (opts, args) => runSkill(opts, args, action),
728
+ ])),
729
+ analytics: Object.fromEntries(Object.keys(ANALYTICS_COMMANDS).map((action) => [
730
+ action,
731
+ (opts, args) => runAnalytics(opts, args, action),
732
+ ])),
440
733
  };
441
734
 
442
735
  async function main() {
@@ -453,7 +746,7 @@ async function main() {
453
746
  // unrecognized flags rather than silently ignoring them — surface that
454
747
  // clearly instead of an unhandled exception.
455
748
  process.stderr.write(`Error: Invalid flag. ${err.message}\n`);
456
- process.exitCode = 1;
749
+ process.exitCode = EXIT_CODES.USAGE;
457
750
  return;
458
751
  }
459
752
 
@@ -462,13 +755,19 @@ async function main() {
462
755
  return;
463
756
  }
464
757
 
758
+ if (positionals[0] === 'register') {
759
+ await runRegister(values);
760
+ return;
761
+ }
762
+
763
+ positionals = normalizeCommand(positionals);
465
764
  const [category, action] = positionals;
466
765
  const categoryRoutes = ROUTES[category];
467
766
 
468
767
  if (!categoryRoutes) {
469
768
  process.stderr.write(`Error: Unknown command category "${category}".\n`);
470
769
  process.stderr.write(`Available categories: ${Object.keys(ROUTES).join(", ")}\n`);
471
- process.exitCode = 1;
770
+ process.exitCode = EXIT_CODES.USAGE;
472
771
  return;
473
772
  }
474
773
 
@@ -480,7 +779,7 @@ async function main() {
480
779
  .map((a) => `${category} ${a}`)
481
780
  .join(", ")}\n`
482
781
  );
483
- process.exitCode = 1;
782
+ process.exitCode = EXIT_CODES.USAGE;
484
783
  return;
485
784
  }
486
785
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixpo/lixblogs-cli",
3
- "version": "1.1.2",
3
+ "version": "1.3.1",
4
4
  "description": "Official CLI for LixBlogs — publish, manage, and inspect blogs through the supported API. Built for creators and agent automation.",
5
5
  "author": {
6
6
  "name": "Elixpo",
@@ -20,12 +20,18 @@
20
20
  "files": [
21
21
  "src",
22
22
  "bin",
23
+ "skills",
23
24
  "README.md",
24
25
  "API.md",
25
- "THREAT_MODEL.md"
26
+ "THREAT_MODEL.md",
27
+ "RELEASE.md",
28
+ "CHANGELOG.md"
26
29
  ],
27
30
  "scripts": {
28
31
  "test": "node --test tests/*.test.mjs",
32
+ "skills:sync": "node scripts/sync-skills.mjs",
33
+ "prepack": "npm run skills:sync && npm test",
34
+ "postpack": "node scripts/sync-skills.mjs --clean",
29
35
  "prepublishOnly": "npm test"
30
36
  },
31
37
  "engines": {
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: lixblogs-analytics
3
+ description: Retrieve and explain aggregate LixBlogs creator analytics through the supported CLI. Use when an agent needs personal or authorized organization performance for an explicit date range, a bounded dimension, or a JSON/CSV export without changing content or exposing visitor identifiers.
4
+ ---
5
+
6
+ # LixBlogs analytics
7
+
8
+ Use `@elixpo/lixblogs-cli` 1.3.0 or newer with `--json --no-input`. Use only CLI commands; never query D1, inspect credentials, or call analytics endpoints directly.
9
+
10
+ ## Access
11
+
12
+ Personal analytics requires `lixblogs:analytics:read`. Organization analytics also requires `lixblogs:organizations:read` and a current owner, admin, or maintain role.
13
+
14
+ ```bash
15
+ lixblogs whoami --json --no-input
16
+ lixblogs analytics query --scope personal --range 30d --dimension overview --json --no-input
17
+ ```
18
+
19
+ Use `lixblogs org list --json --no-input` before querying `--scope org:ORG_ID`. Never infer organization access from a public page or slug.
20
+
21
+ ## Query
22
+
23
+ Choose one range and one dimension per request. Supported ranges are `7d`, `30d`, `90d`, `12m`, or `custom`; custom requires both ISO dates. Supported dimensions are `overview`, `timeline`, `posts`, `sources`, `devices`, and `countries`.
24
+
25
+ ```bash
26
+ lixblogs analytics query --range custom --from 2026-07-01 --to 2026-07-31 \
27
+ --dimension posts --limit 25 --json --no-input
28
+ ```
29
+
30
+ Follow `meta.nextCursor` until null when all pages are required. Keep `--limit` at 100 or below. Treat an empty `values` array as valid data, not a failure.
31
+
32
+ ## Explain results
33
+
34
+ - State the scope and date range first.
35
+ - Report returned facts separately from interpretations.
36
+ - Use the returned metric definitions; do not redefine a metric.
37
+ - Call out a zero previous value when describing percentage changes.
38
+ - Do not rank creators, infer sensitive traits, or attempt to identify visitors.
39
+ - Say when a collection window is incomplete or the dataset is empty.
40
+
41
+ ## Export
42
+
43
+ ```bash
44
+ lixblogs analytics export --scope personal --range 30d --dimension timeline \
45
+ --format csv --output analytics.csv --json --no-input
46
+ ```
47
+
48
+ Exports support JSON and CSV and refuse to overwrite an existing path. Choose a new path instead of deleting or replacing a file.
49
+
50
+ ## Recovery
51
+
52
+ - `insufficient_scope`: log in again with only the missing read scope.
53
+ - `forbidden_scope`: stop; do not probe another organization identifier.
54
+ - `invalid_request`, `invalid_limit`, or `invalid_cursor`: correct the explicit query and retry once.
55
+ - `rate_limit_exceeded`: honor the server retry window.
56
+ - Preserve request IDs for diagnosis without printing tokens or credentials.
57
+
58
+ This skill performs no blog, collaborator, organization, or account mutation.
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "LixBlogs Analytics"
3
+ short_description: "Read bounded creator performance data"
4
+ brand_color: "#8B5CF6"
5
+ default_prompt: "Use $lixblogs-analytics to retrieve and explain creator analytics without making changes."
6
+ policy:
7
+ allow_implicit_invocation: true
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: lixblogs-author
3
+ description: Draft, inspect, and revise LixBlogs posts through the supported CLI. Use when an agent needs to outline a post, create a draft, update Markdown or metadata, or resolve an edit conflict without publishing or deleting content.
4
+ ---
5
+
6
+ # LixBlogs author
7
+
8
+ Use `@elixpo/lixblogs-cli` 1.2.0 or newer. Run every automation command with `--json --no-input`. Never use D1, session cookies, passwords, bearer tokens, or direct API calls.
9
+
10
+ ## Access
11
+
12
+ - Inspect: `lixblogs:profile:read`, `lixblogs:blog:read`
13
+ - Draft or revise: add `lixblogs:blog:write`
14
+ - Do not request publish, delete, organization-write, or collaboration-write scopes for this workflow.
15
+
16
+ Check the active identity before writing:
17
+
18
+ ```bash
19
+ lixblogs whoami --json --no-input
20
+ ```
21
+
22
+ If a scope is missing, stop and tell the user which scope is required. Do not initiate login without their involvement.
23
+
24
+ ## Workflow
25
+
26
+ 1. Inspect existing work with `lixblogs blog list --status draft --json --no-input` or `lixblogs blog get BLOG_ID --json --no-input`.
27
+ 2. Preserve the creator's claims, citations, tone, headings, and code. Mark unsupported facts for review; do not invent them.
28
+ 3. Prefer a Markdown file for substantial content. Use stdin for a generated pipeline and `--editor` only for a human-controlled terminal.
29
+ 4. Validate the intended write with `--dry-run`.
30
+ 5. Write the draft, then fetch it once to verify the stored result.
31
+
32
+ ```bash
33
+ lixblogs blog create --file post.md --title "Title" --tag topic --dry-run --json --no-input
34
+ lixblogs blog create --file post.md --title "Title" --tag topic --json --no-input
35
+ lixblogs blog edit BLOG_ID --file post.md --dry-run --json --no-input
36
+ lixblogs blog edit BLOG_ID --file post.md --json --no-input
37
+ ```
38
+
39
+ Metadata-only revisions use `--title`, `--subtitle`, `--slug`, repeatable `--tag`, `--emoji`, `--cover`, `--publication`, and `--collection`. Content inputs `--file`, `--stdin`, `--content`, and `--editor` are mutually exclusive.
40
+
41
+ ## Recovery
42
+
43
+ - Exit `2`: correct command syntax; never guess a flag.
44
+ - Exit `3` / `revision_conflict`: read `details.localPath` and `details.serverPath`, preserve both authors' changes, obtain creator approval for ambiguous merges, then retry with `--etag details.serverEtag`.
45
+ - Exit `4` / authentication or scope failure: stop and request the named login/scope action.
46
+ - A request ID in an error is diagnostic metadata; report it without exposing credentials.
47
+
48
+ This skill ends at a reviewed draft. Use the separate `lixblogs-publish` skill for any public-state change.