@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/API.md +18 -0
- package/CHANGELOG.md +10 -0
- package/README.md +107 -17
- package/RELEASE.md +30 -0
- package/bin/lixblogs.mjs +343 -44
- package/package.json +8 -2
- package/skills/lixblogs-analytics/SKILL.md +58 -0
- package/skills/lixblogs-analytics/agents/openai.yaml +7 -0
- package/skills/lixblogs-author/SKILL.md +48 -0
- package/skills/lixblogs-author/agents/openai.yaml +7 -0
- package/skills/lixblogs-editorial/SKILL.md +54 -0
- package/skills/lixblogs-editorial/agents/openai.yaml +7 -0
- package/skills/lixblogs-organizations/SKILL.md +44 -0
- package/skills/lixblogs-organizations/agents/openai.yaml +7 -0
- package/skills/lixblogs-publish/SKILL.md +55 -0
- package/skills/lixblogs-publish/agents/openai.yaml +7 -0
- package/src/api/AnalyticsClient.js +40 -0
- package/src/api/BlogClient.js +5 -0
- package/src/api/CollaborationClient.js +73 -0
- package/src/api/OrgClient.js +158 -0
- package/src/auth/AuthenticatedClient.js +15 -0
- package/src/auth/ElixpoAuthProvider.js +1 -1
- package/src/cli/contract.js +46 -0
- package/src/cli/ui.js +52 -0
- package/src/commands/analytics/index.js +57 -0
- package/src/commands/blog/index.js +4 -0
- package/src/commands/collab/index.js +53 -0
- package/src/commands/org/index.js +22 -0
- package/src/commands/skill/index.js +83 -0
- package/src/config/providerFactory.js +1 -1
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>
|
|
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
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
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,
|
|
173
|
-
const
|
|
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(
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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(`
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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.
|