@elixpo/lixblogs-cli 1.1.2 → 1.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/API.md +18 -0
- package/CHANGELOG.md +10 -0
- package/README.md +111 -18
- package/RELEASE.md +30 -0
- package/bin/lixblogs.mjs +358 -46
- 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 +54 -0
- package/src/commands/analytics/index.js +57 -0
- package/src/commands/auth/login.js +16 -2
- package/src/commands/auth/profileAlias.js +29 -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
|
@@ -31,9 +31,15 @@ import { authStatus } from "../src/commands/auth/status.js";
|
|
|
31
31
|
import { authLogout } from "../src/commands/auth/logout.js";
|
|
32
32
|
import { authRevoke } from "../src/commands/auth/revoke.js";
|
|
33
33
|
import { authProfiles, authUse } from "../src/commands/auth/profiles.js";
|
|
34
|
+
import { profileAliasFromIdentity } from "../src/commands/auth/profileAlias.js";
|
|
34
35
|
import { ProfileRegistry, validateProfileId } from "../src/config/ProfileRegistry.js";
|
|
35
36
|
import { AuthenticatedClient } from "../src/auth/AuthenticatedClient.js";
|
|
36
37
|
import { BlogClient, BlogApiError } from "../src/api/BlogClient.js";
|
|
38
|
+
import { OrgClient } from "../src/api/OrgClient.js";
|
|
39
|
+
import { CollaborationClient } from "../src/api/CollaborationClient.js";
|
|
40
|
+
import { AnalyticsClient } from "../src/api/AnalyticsClient.js";
|
|
41
|
+
import { EXIT_CODES, errorEnvelope, normalizeCommand } from "../src/cli/contract.js";
|
|
42
|
+
import { colorEnabled, listenForEnter, loginChallenge, successLine } from "../src/cli/ui.js";
|
|
37
43
|
import {
|
|
38
44
|
blogCreate,
|
|
39
45
|
blogDelete,
|
|
@@ -44,6 +50,24 @@ import {
|
|
|
44
50
|
blogRestore,
|
|
45
51
|
blogUnpublish,
|
|
46
52
|
} from "../src/commands/blog/index.js";
|
|
53
|
+
import {
|
|
54
|
+
orgCollections,
|
|
55
|
+
orgGet,
|
|
56
|
+
orgList,
|
|
57
|
+
orgMembers,
|
|
58
|
+
orgTargets,
|
|
59
|
+
} from "../src/commands/org/index.js";
|
|
60
|
+
import {
|
|
61
|
+
collabAccept,
|
|
62
|
+
collabDecline,
|
|
63
|
+
collabInvitations,
|
|
64
|
+
collabInvite,
|
|
65
|
+
collabList,
|
|
66
|
+
collabRemove,
|
|
67
|
+
collabRole,
|
|
68
|
+
} from "../src/commands/collab/index.js";
|
|
69
|
+
import { skillInspect, skillInstall, skillList } from "../src/commands/skill/index.js";
|
|
70
|
+
import { analyticsExport, analyticsQuery } from "../src/commands/analytics/index.js";
|
|
47
71
|
|
|
48
72
|
const OPTIONS = {
|
|
49
73
|
profile: { type: "string" },
|
|
@@ -62,6 +86,12 @@ const OPTIONS = {
|
|
|
62
86
|
status: { type: "string" },
|
|
63
87
|
limit: { type: "string" },
|
|
64
88
|
cursor: { type: "string" },
|
|
89
|
+
range: { type: "string" },
|
|
90
|
+
from: { type: "string" },
|
|
91
|
+
to: { type: "string" },
|
|
92
|
+
dimension: { type: "string" },
|
|
93
|
+
format: { type: "string" },
|
|
94
|
+
output: { type: "string" },
|
|
65
95
|
file: { type: "string" },
|
|
66
96
|
stdin: { type: "boolean", default: false },
|
|
67
97
|
content: { type: "string" },
|
|
@@ -83,12 +113,23 @@ const OPTIONS = {
|
|
|
83
113
|
etag: { type: "string" },
|
|
84
114
|
permanent: { type: "boolean", default: false },
|
|
85
115
|
"idempotency-key": { type: "string" },
|
|
116
|
+
user: { type: "string" },
|
|
117
|
+
role: { type: "string" },
|
|
118
|
+
"hide-on-profile": { type: "boolean", default: false },
|
|
119
|
+
target: { type: "string" },
|
|
120
|
+
force: { type: "boolean", default: false },
|
|
86
121
|
help: { type: "boolean", short: "h", default: false },
|
|
87
122
|
};
|
|
88
123
|
|
|
89
124
|
const HELP_TEXT = `lixblogs — LixBlogs CLI
|
|
90
125
|
|
|
91
126
|
Usage:
|
|
127
|
+
lixblogs login [--profile <name>] [--open]
|
|
128
|
+
lixblogs register [--profile <name>] [--open]
|
|
129
|
+
lixblogs logout [--profile <name>]
|
|
130
|
+
lixblogs whoami [--profile <name>] [--json]
|
|
131
|
+
lixblogs profiles [--json]
|
|
132
|
+
lixblogs use <name> [--json]
|
|
92
133
|
lixblogs auth login [--profile <name>] [--env <environment>] [--json] [--quiet] [--allow-insecure-fallback]
|
|
93
134
|
lixblogs auth status [--profile <name>] [--json]
|
|
94
135
|
lixblogs auth logout [--profile <name>] [--json] [--quiet]
|
|
@@ -97,15 +138,34 @@ Usage:
|
|
|
97
138
|
lixblogs auth use <name> [--json]
|
|
98
139
|
lixblogs blog list [--status <status>] [--limit <n>] [--cursor <cursor>] [--json]
|
|
99
140
|
lixblogs blog get <id> [--json]
|
|
141
|
+
lixblogs blog preview <id> [--json]
|
|
100
142
|
lixblogs blog create [--file <post.md>|--stdin|--content <markdown>|--editor] [metadata]
|
|
101
143
|
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]
|
|
144
|
+
lixblogs blog publish <id> --yes [--dry-run] [--json]
|
|
145
|
+
lixblogs blog unpublish <id> --yes [--dry-run] [--json]
|
|
104
146
|
lixblogs blog delete <id> --yes [--permanent] [--dry-run] [--json]
|
|
105
|
-
lixblogs blog
|
|
147
|
+
lixblogs blog trash <id> --yes [--dry-run] [--json]
|
|
148
|
+
lixblogs blog restore <id> --yes [--dry-run] [--json]
|
|
149
|
+
lixblogs org list [--json]
|
|
150
|
+
lixblogs org get <id> [--json]
|
|
151
|
+
lixblogs org collections <id> [--json]
|
|
152
|
+
lixblogs org members <id> [--json]
|
|
153
|
+
lixblogs org targets [--json]
|
|
154
|
+
lixblogs collab list <blog-id> [--json]
|
|
155
|
+
lixblogs collab invitations [--json]
|
|
156
|
+
lixblogs collab invite <blog-id> --user <username> --role <viewer|editor|admin> --yes
|
|
157
|
+
lixblogs collab role <blog-id> --user <username-or-id> --role <viewer|editor|admin> --yes
|
|
158
|
+
lixblogs collab remove <blog-id> [--user <username-or-id>] --yes
|
|
159
|
+
lixblogs collab accept <blog-id> --yes [--hide-on-profile]
|
|
160
|
+
lixblogs collab decline <blog-id> --yes
|
|
161
|
+
lixblogs analytics query [--scope personal|org:<id>] [--range 30d] [--dimension overview]
|
|
162
|
+
lixblogs analytics export --output <file> [--format json|csv] [query options]
|
|
163
|
+
lixblogs skill list [--json]
|
|
164
|
+
lixblogs skill inspect <name> [--json]
|
|
165
|
+
lixblogs skill install <name> [--target <directory>] [--dry-run] --yes
|
|
106
166
|
|
|
107
167
|
Global flags:
|
|
108
|
-
--profile <name>
|
|
168
|
+
--profile <name> local account alias (defaults to the signed-in username)
|
|
109
169
|
--env <environment> override environment (development|staging|production)
|
|
110
170
|
--auth-provider <provider> elixpo, or mock in development/test only
|
|
111
171
|
--accounts-url <url> override the Accounts discovery origin
|
|
@@ -121,7 +181,7 @@ Global flags:
|
|
|
121
181
|
--collection <id> organization collection ID
|
|
122
182
|
--dry-run validate and show the intended action without writing
|
|
123
183
|
--permanent permanently delete instead of moving to trash
|
|
124
|
-
--open open the
|
|
184
|
+
--open open the device verification URL immediately
|
|
125
185
|
--json machine-readable JSON output
|
|
126
186
|
--quiet suppress non-essential output
|
|
127
187
|
--yes, -y auto-confirm destructive actions (required for revoke)
|
|
@@ -129,8 +189,9 @@ Global flags:
|
|
|
129
189
|
non-persistent in-memory store instead of failing
|
|
130
190
|
--help, -h show this help
|
|
131
191
|
|
|
132
|
-
|
|
133
|
-
|
|
192
|
+
Machine mode:
|
|
193
|
+
--json --no-input produces stable JSON on stdout, diagnostics on stderr, and
|
|
194
|
+
never prompts. Publishing and destructive state changes require --yes.
|
|
134
195
|
`;
|
|
135
196
|
|
|
136
197
|
const DEFAULT_SCOPES = [
|
|
@@ -169,14 +230,18 @@ function output(opts, data) {
|
|
|
169
230
|
}
|
|
170
231
|
}
|
|
171
232
|
|
|
172
|
-
function fail(opts,
|
|
173
|
-
const
|
|
233
|
+
function fail(opts, error, exitCode = EXIT_CODES.ERROR) {
|
|
234
|
+
const value = error && typeof error === 'object' ? error : { message: String(error) };
|
|
235
|
+
const safeMessage = redactErrorMessage(value.message);
|
|
236
|
+
const envelope = errorEnvelope({ ...value, message: safeMessage });
|
|
174
237
|
if (opts.json) {
|
|
175
|
-
process.stdout.write(safeJsonStringify(
|
|
238
|
+
process.stdout.write(safeJsonStringify(envelope) + "\n");
|
|
176
239
|
} else if (!opts.quiet) {
|
|
177
240
|
process.stderr.write(`Error: ${safeMessage}\n`);
|
|
241
|
+
if (value.hint) process.stderr.write(`Hint: ${value.hint}\n`);
|
|
242
|
+
if (value.requestId) process.stderr.write(`Request: ${value.requestId}\n`);
|
|
178
243
|
}
|
|
179
|
-
process.exitCode = exitCode;
|
|
244
|
+
process.exitCode = value.exitCode || exitCode;
|
|
180
245
|
}
|
|
181
246
|
|
|
182
247
|
/**
|
|
@@ -205,7 +270,11 @@ async function getCredentialStoreOrFail(opts, profileRegistry) {
|
|
|
205
270
|
async function runLogin(opts) {
|
|
206
271
|
const config = resolveConfig({ flags: configFlags(opts) });
|
|
207
272
|
const profileRegistry = new ProfileRegistry();
|
|
208
|
-
const
|
|
273
|
+
const requestedProfileId = validateProfileId(config.profile);
|
|
274
|
+
const scopes = opts.scope?.length ? [...opts.scope] : [...DEFAULT_SCOPES];
|
|
275
|
+
if (!config.profileExplicit && !scopes.includes("lixblogs:profile:read")) {
|
|
276
|
+
scopes.push("lixblogs:profile:read");
|
|
277
|
+
}
|
|
209
278
|
|
|
210
279
|
let provider;
|
|
211
280
|
try {
|
|
@@ -217,35 +286,53 @@ async function runLogin(opts) {
|
|
|
217
286
|
const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
|
|
218
287
|
if (!credentialStore) return;
|
|
219
288
|
|
|
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
|
-
|
|
289
|
+
let stopEnterListener = () => {};
|
|
290
|
+
let result;
|
|
291
|
+
try {
|
|
292
|
+
result = await authLogin({
|
|
293
|
+
provider,
|
|
294
|
+
credentialStore,
|
|
295
|
+
profileId: requestedProfileId,
|
|
296
|
+
scopes,
|
|
297
|
+
openBrowser: opts.open ? openBrowser : undefined,
|
|
298
|
+
resolveProfileId: config.profileExplicit
|
|
299
|
+
? undefined
|
|
300
|
+
: ({ accessToken }) => profileAliasFromIdentity({
|
|
301
|
+
accessToken,
|
|
302
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
303
|
+
}),
|
|
304
|
+
onStatus: (status) => {
|
|
305
|
+
if (opts.json) {
|
|
306
|
+
if (status.type !== "pending") output(opts, { event: status.type, ...status });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (opts.quiet) return;
|
|
310
|
+
if (status.type === "verification_pending") {
|
|
311
|
+
const url = status.verificationUriComplete || status.verificationUri;
|
|
312
|
+
const interactive = Boolean(process.stdin.isTTY) && !opts["no-input"];
|
|
313
|
+
process.stdout.write(loginChallenge({
|
|
314
|
+
url,
|
|
315
|
+
code: status.userCode,
|
|
316
|
+
expiresInSeconds: status.expiresInSeconds,
|
|
317
|
+
profile: config.profileExplicit ? requestedProfileId : null,
|
|
318
|
+
interactive,
|
|
319
|
+
color: colorEnabled(),
|
|
320
|
+
}));
|
|
321
|
+
if (interactive && !opts.open) {
|
|
322
|
+
stopEnterListener = listenForEnter({ input: process.stdin, open: openBrowser, url });
|
|
323
|
+
}
|
|
324
|
+
} else if (status.type === "approved") {
|
|
325
|
+
console.log(successLine("Access approved by Elixpo Accounts.", colorEnabled()));
|
|
326
|
+
} else if (status.type === "denied") {
|
|
327
|
+
console.log(" Access denied.");
|
|
328
|
+
} else if (status.type === "expired") {
|
|
329
|
+
console.log(" Device code expired.");
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
} finally {
|
|
334
|
+
stopEnterListener();
|
|
335
|
+
}
|
|
249
336
|
|
|
250
337
|
if (!result.ok) {
|
|
251
338
|
return fail(opts, result.reason);
|
|
@@ -254,7 +341,9 @@ async function runLogin(opts) {
|
|
|
254
341
|
await profileRegistry.setActive(result.profileId);
|
|
255
342
|
output(opts, { ok: true, profile: result.profileId });
|
|
256
343
|
if (!opts.json && !opts.quiet) {
|
|
257
|
-
console.log(`
|
|
344
|
+
console.log(` Credentials saved to local profile "${result.profileId}".`);
|
|
345
|
+
console.log(" Tip: add another account with `lixblogs login`, list accounts with `lixblogs profiles`,");
|
|
346
|
+
console.log(" and switch with `lixblogs use <username>`.");
|
|
258
347
|
}
|
|
259
348
|
}
|
|
260
349
|
|
|
@@ -281,6 +370,60 @@ async function runStatus(opts) {
|
|
|
281
370
|
}
|
|
282
371
|
}
|
|
283
372
|
|
|
373
|
+
async function authenticatedBlogClient(opts) {
|
|
374
|
+
const config = resolveConfig({ flags: configFlags(opts) });
|
|
375
|
+
const profileRegistry = new ProfileRegistry();
|
|
376
|
+
const profileId = await selectedProfile(config, profileRegistry);
|
|
377
|
+
const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
|
|
378
|
+
if (!credentialStore) return null;
|
|
379
|
+
let provider;
|
|
380
|
+
try { provider = createAuthProvider(config); } catch (error) { fail(opts, error); return null; }
|
|
381
|
+
const http = new AuthenticatedClient({ provider, credentialStore, profileId, apiBaseUrl: config.apiBaseUrl });
|
|
382
|
+
return { client: new BlogClient(http), http, config, credentialStore, profileId };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function runWhoami(opts) {
|
|
386
|
+
const context = await authenticatedBlogClient(opts);
|
|
387
|
+
if (!context) return;
|
|
388
|
+
try {
|
|
389
|
+
const [identity, credentials] = await Promise.all([
|
|
390
|
+
context.client.whoami(),
|
|
391
|
+
context.credentialStore.get(context.profileId),
|
|
392
|
+
]);
|
|
393
|
+
const result = {
|
|
394
|
+
ok: true,
|
|
395
|
+
profile: context.profileId,
|
|
396
|
+
environment: context.config.environment,
|
|
397
|
+
identity,
|
|
398
|
+
scopes: credentials?.scopes || [],
|
|
399
|
+
expiresAt: credentials?.expiresAt ? new Date(credentials.expiresAt).toISOString() : null,
|
|
400
|
+
expired: credentials ? Date.now() >= credentials.expiresAt : true,
|
|
401
|
+
};
|
|
402
|
+
output(opts, result);
|
|
403
|
+
if (!opts.json && !opts.quiet) {
|
|
404
|
+
console.log(`${identity.displayName || identity.username} (@${identity.username})`);
|
|
405
|
+
console.log(`Profile: ${context.profileId} · ${result.environment}`);
|
|
406
|
+
console.log(`Scopes: ${result.scopes.join(', ') || 'none'}`);
|
|
407
|
+
console.log(`Expires: ${result.expiresAt || 'unknown'}`);
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function runRegister(opts) {
|
|
415
|
+
const config = resolveConfig({ flags: configFlags(opts) });
|
|
416
|
+
const registrationUrl = new URL('/register', config.accountsBaseUrl).toString();
|
|
417
|
+
if (opts['no-input']) {
|
|
418
|
+
output(opts, { ok: true, registrationUrl, next: 'lixblogs login' });
|
|
419
|
+
if (!opts.json && !opts.quiet) console.log(registrationUrl);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await openBrowser(registrationUrl);
|
|
423
|
+
if (!opts.quiet) console.log(`Create your account at ${registrationUrl}, then approve the device login.`);
|
|
424
|
+
await runLogin(opts);
|
|
425
|
+
}
|
|
426
|
+
|
|
284
427
|
async function runLogout(opts) {
|
|
285
428
|
const config = resolveConfig({ flags: configFlags(opts) });
|
|
286
429
|
const profileRegistry = new ProfileRegistry();
|
|
@@ -370,14 +513,45 @@ async function runUse(opts, args) {
|
|
|
370
513
|
const BLOG_COMMANDS = {
|
|
371
514
|
list: blogList,
|
|
372
515
|
get: blogGet,
|
|
516
|
+
preview: blogGet,
|
|
373
517
|
create: blogCreate,
|
|
374
518
|
edit: blogEdit,
|
|
375
519
|
publish: blogPublish,
|
|
376
520
|
unpublish: blogUnpublish,
|
|
377
521
|
delete: blogDelete,
|
|
522
|
+
trash: blogDelete,
|
|
378
523
|
restore: blogRestore,
|
|
379
524
|
};
|
|
380
525
|
|
|
526
|
+
const ORG_COMMANDS = {
|
|
527
|
+
list: orgList,
|
|
528
|
+
get: orgGet,
|
|
529
|
+
collections: orgCollections,
|
|
530
|
+
members: orgMembers,
|
|
531
|
+
targets: orgTargets,
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const COLLAB_COMMANDS = {
|
|
535
|
+
list: collabList,
|
|
536
|
+
invitations: collabInvitations,
|
|
537
|
+
invite: collabInvite,
|
|
538
|
+
role: collabRole,
|
|
539
|
+
remove: collabRemove,
|
|
540
|
+
accept: collabAccept,
|
|
541
|
+
decline: collabDecline,
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const SKILL_COMMANDS = {
|
|
545
|
+
list: ({ options }) => skillList(options),
|
|
546
|
+
inspect: ({ id }) => skillInspect({ name: id }),
|
|
547
|
+
install: ({ id, options }) => skillInstall({ name: id, options }),
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const ANALYTICS_COMMANDS = {
|
|
551
|
+
query: analyticsQuery,
|
|
552
|
+
export: analyticsExport,
|
|
553
|
+
};
|
|
554
|
+
|
|
381
555
|
async function runBlog(opts, args, action) {
|
|
382
556
|
const config = resolveConfig({ flags: configFlags(opts) });
|
|
383
557
|
const profileRegistry = new ProfileRegistry();
|
|
@@ -420,7 +594,122 @@ async function runBlog(opts, args, action) {
|
|
|
420
594
|
process.exitCode = error.status === 412 ? 3 : 1;
|
|
421
595
|
return;
|
|
422
596
|
}
|
|
423
|
-
return fail(opts,
|
|
597
|
+
return fail(opts, error, error.status === 412 ? EXIT_CODES.CONFLICT : EXIT_CODES.ERROR);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function runOrg(opts, args, action) {
|
|
602
|
+
const context = await authenticatedBlogClient(opts);
|
|
603
|
+
if (!context) return;
|
|
604
|
+
const client = new OrgClient(context.http);
|
|
605
|
+
try {
|
|
606
|
+
const result = await ORG_COMMANDS[action]({ client, id: args[0], options: opts });
|
|
607
|
+
output(opts, { ok: true, data: result });
|
|
608
|
+
if (opts.json || opts.quiet) return;
|
|
609
|
+
if (action === 'targets') {
|
|
610
|
+
console.log('personal\tPersonal Blog');
|
|
611
|
+
for (const org of result.organizations || []) {
|
|
612
|
+
console.log(`${org.target}\t${org.role}\t${org.name}`);
|
|
613
|
+
for (const collection of org.collections || []) {
|
|
614
|
+
console.log(` collection:${collection.id}\t${collection.name}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const rows = action === 'list' ? result.data || [] : Array.isArray(result) ? result : [result];
|
|
620
|
+
for (const row of rows) {
|
|
621
|
+
console.log([
|
|
622
|
+
row.id || row.userId || row.orgId,
|
|
623
|
+
row.role,
|
|
624
|
+
row.slug || row.username,
|
|
625
|
+
row.name || row.displayName,
|
|
626
|
+
].filter(Boolean).join('\t'));
|
|
627
|
+
}
|
|
628
|
+
} catch (error) {
|
|
629
|
+
fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function runCollab(opts, args, action) {
|
|
634
|
+
const context = await authenticatedBlogClient(opts);
|
|
635
|
+
if (!context) return;
|
|
636
|
+
const client = new CollaborationClient(context.http);
|
|
637
|
+
try {
|
|
638
|
+
const result = await COLLAB_COMMANDS[action]({ client, id: args[0], options: opts });
|
|
639
|
+
output(opts, { ok: true, data: result });
|
|
640
|
+
if (opts.json || opts.quiet) return;
|
|
641
|
+
if (result.dryRun) {
|
|
642
|
+
console.log(`Dry run: ${result.action} validated; no changes sent.`);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const rows = action === 'invitations'
|
|
646
|
+
? result
|
|
647
|
+
: action === 'list'
|
|
648
|
+
? result.collaborators || []
|
|
649
|
+
: [result];
|
|
650
|
+
for (const row of rows) {
|
|
651
|
+
console.log([
|
|
652
|
+
row.blogId || row.userId,
|
|
653
|
+
row.status,
|
|
654
|
+
row.role,
|
|
655
|
+
row.username || row.title,
|
|
656
|
+
row.notificationState,
|
|
657
|
+
].filter(Boolean).join('\t'));
|
|
658
|
+
}
|
|
659
|
+
} catch (error) {
|
|
660
|
+
fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function runSkill(opts, args, action) {
|
|
665
|
+
try {
|
|
666
|
+
const result = await SKILL_COMMANDS[action]({ id: args[0], options: opts });
|
|
667
|
+
output(opts, { ok: true, data: result });
|
|
668
|
+
if (opts.json || opts.quiet) return;
|
|
669
|
+
if (action === 'list') {
|
|
670
|
+
for (const skill of result) console.log(`${skill.name}\tCLI >= ${skill.minimumCliVersion || 'unknown'}\t${skill.description}`);
|
|
671
|
+
} else if (action === 'inspect') {
|
|
672
|
+
process.stdout.write(result.content);
|
|
673
|
+
} else if (result.dryRun) {
|
|
674
|
+
console.log(`Dry run: install ${result.name} to ${result.target}${result.replace ? ' (replace)' : ''}.`);
|
|
675
|
+
} else {
|
|
676
|
+
console.log(`Installed ${result.name} at ${result.target}.`);
|
|
677
|
+
}
|
|
678
|
+
} catch (error) {
|
|
679
|
+
fail(opts, error);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function runAnalytics(opts, _args, action) {
|
|
684
|
+
const context = await authenticatedBlogClient(opts);
|
|
685
|
+
if (!context) return;
|
|
686
|
+
const client = new AnalyticsClient(context.http);
|
|
687
|
+
const normalized = {
|
|
688
|
+
...opts,
|
|
689
|
+
limit: opts.limit === undefined ? undefined : Number.parseInt(opts.limit, 10),
|
|
690
|
+
};
|
|
691
|
+
try {
|
|
692
|
+
const result = await ANALYTICS_COMMANDS[action]({ client, options: normalized });
|
|
693
|
+
output(opts, { ok: true, data: result });
|
|
694
|
+
if (opts.json || opts.quiet) return;
|
|
695
|
+
if (action === 'export') {
|
|
696
|
+
console.log(`Exported ${result.rows} rows to ${result.output}.`);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const payload = result.data;
|
|
700
|
+
console.log(`${payload.scope.label} · ${payload.dimension} · ${payload.range.key}`);
|
|
701
|
+
if (payload.dimension === 'overview') {
|
|
702
|
+
for (const [metric, value] of Object.entries(payload.values.totals)) {
|
|
703
|
+
console.log(`${metric}\t${value}\t${payload.values.changes[metric]}%`);
|
|
704
|
+
}
|
|
705
|
+
} else if (payload.dimension === 'timeline') {
|
|
706
|
+
payload.values.labels.forEach((label, index) => console.log(`${label}\t${payload.values.views[index]}\t${payload.values.reads[index]}`));
|
|
707
|
+
} else {
|
|
708
|
+
for (const row of payload.values) console.log(Object.values(row).join('\t'));
|
|
709
|
+
if (result.meta?.nextCursor) console.log(`Next cursor: ${result.meta.nextCursor}`);
|
|
710
|
+
}
|
|
711
|
+
} catch (error) {
|
|
712
|
+
fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
|
|
424
713
|
}
|
|
425
714
|
}
|
|
426
715
|
|
|
@@ -428,6 +717,7 @@ const ROUTES = {
|
|
|
428
717
|
auth: {
|
|
429
718
|
login: runLogin,
|
|
430
719
|
status: runStatus,
|
|
720
|
+
whoami: runWhoami,
|
|
431
721
|
logout: runLogout,
|
|
432
722
|
revoke: runRevoke,
|
|
433
723
|
profiles: runProfiles,
|
|
@@ -437,6 +727,22 @@ const ROUTES = {
|
|
|
437
727
|
action,
|
|
438
728
|
(opts, args) => runBlog(opts, args, action),
|
|
439
729
|
])),
|
|
730
|
+
org: Object.fromEntries(Object.keys(ORG_COMMANDS).map((action) => [
|
|
731
|
+
action,
|
|
732
|
+
(opts, args) => runOrg(opts, args, action),
|
|
733
|
+
])),
|
|
734
|
+
collab: Object.fromEntries(Object.keys(COLLAB_COMMANDS).map((action) => [
|
|
735
|
+
action,
|
|
736
|
+
(opts, args) => runCollab(opts, args, action),
|
|
737
|
+
])),
|
|
738
|
+
skill: Object.fromEntries(Object.keys(SKILL_COMMANDS).map((action) => [
|
|
739
|
+
action,
|
|
740
|
+
(opts, args) => runSkill(opts, args, action),
|
|
741
|
+
])),
|
|
742
|
+
analytics: Object.fromEntries(Object.keys(ANALYTICS_COMMANDS).map((action) => [
|
|
743
|
+
action,
|
|
744
|
+
(opts, args) => runAnalytics(opts, args, action),
|
|
745
|
+
])),
|
|
440
746
|
};
|
|
441
747
|
|
|
442
748
|
async function main() {
|
|
@@ -453,7 +759,7 @@ async function main() {
|
|
|
453
759
|
// unrecognized flags rather than silently ignoring them — surface that
|
|
454
760
|
// clearly instead of an unhandled exception.
|
|
455
761
|
process.stderr.write(`Error: Invalid flag. ${err.message}\n`);
|
|
456
|
-
process.exitCode =
|
|
762
|
+
process.exitCode = EXIT_CODES.USAGE;
|
|
457
763
|
return;
|
|
458
764
|
}
|
|
459
765
|
|
|
@@ -462,13 +768,19 @@ async function main() {
|
|
|
462
768
|
return;
|
|
463
769
|
}
|
|
464
770
|
|
|
771
|
+
if (positionals[0] === 'register') {
|
|
772
|
+
await runRegister(values);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
positionals = normalizeCommand(positionals);
|
|
465
777
|
const [category, action] = positionals;
|
|
466
778
|
const categoryRoutes = ROUTES[category];
|
|
467
779
|
|
|
468
780
|
if (!categoryRoutes) {
|
|
469
781
|
process.stderr.write(`Error: Unknown command category "${category}".\n`);
|
|
470
782
|
process.stderr.write(`Available categories: ${Object.keys(ROUTES).join(", ")}\n`);
|
|
471
|
-
process.exitCode =
|
|
783
|
+
process.exitCode = EXIT_CODES.USAGE;
|
|
472
784
|
return;
|
|
473
785
|
}
|
|
474
786
|
|
|
@@ -480,7 +792,7 @@ async function main() {
|
|
|
480
792
|
.map((a) => `${category} ${a}`)
|
|
481
793
|
.join(", ")}\n`
|
|
482
794
|
);
|
|
483
|
-
process.exitCode =
|
|
795
|
+
process.exitCode = EXIT_CODES.USAGE;
|
|
484
796
|
return;
|
|
485
797
|
}
|
|
486
798
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elixpo/lixblogs-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.3",
|
|
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
|