@neta-art/cohub-cli 3.10.0 → 3.10.2

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.
@@ -169,6 +169,14 @@ function uploadFailure(errors) {
169
169
  const first = errors[0]?.message ?? "Upload failed";
170
170
  return new Error(`${first} (${errors.length} files failed)`);
171
171
  }
172
+ function publicUrlPrefix(destination, entry) {
173
+ const encodedPath = entry.path.split("/").map(encodeURIComponent).join("/");
174
+ if (!entry.publicUrl.endsWith(encodedPath)) {
175
+ throw new Error(`Invalid public URL for ${entry.path}`);
176
+ }
177
+ const encodedDestination = destination.split("/").map(encodeURIComponent).join("/");
178
+ return `${entry.publicUrl.slice(0, -encodedPath.length)}${encodedDestination}`;
179
+ }
172
180
  async function uploadPublic(command, source, destination, opts, deps) {
173
181
  const client = deps.createClient?.() ?? createClient();
174
182
  const spaceId = resolveSpace(command);
@@ -206,17 +214,28 @@ async function uploadPublic(command, source, destination, opts, deps) {
206
214
  if (entryErrors.length > 0)
207
215
  throw uploadFailure(entryErrors);
208
216
  }
209
- const entryUrl = entryPlan?.publicUrl ?? null;
217
+ const firstPlan = plan.entries[0];
218
+ if (!firstPlan)
219
+ throw new Error("Upload plan contains no files");
210
220
  if (jsonRequested(opts)) {
211
221
  outJson({
212
- uploaded: upload.files.length,
213
- overwrite: Boolean(opts.overwrite),
214
222
  destination: upload.destination,
215
- url: entryUrl,
223
+ urlPrefix: publicUrlPrefix("", firstPlan),
224
+ files: upload.files.map((file) => ({
225
+ path: file.publicPath,
226
+ size: file.size,
227
+ mimeType: file.mimeType,
228
+ })),
216
229
  });
217
230
  return;
218
231
  }
219
- console.log(entryUrl ?? `Uploaded ${upload.files.length} files to ${upload.destination}`);
232
+ if (entryPlan) {
233
+ console.log(entryPlan.publicUrl);
234
+ return;
235
+ }
236
+ const fileLabel = upload.files.length === 1 ? "file" : "files";
237
+ console.log(`Uploaded ${upload.files.length} ${fileLabel} to ${upload.destination}`);
238
+ console.log(`URL prefix: ${publicUrlPrefix(upload.destination, firstPlan)}`);
220
239
  }
221
240
  catch (exception) {
222
241
  handleHttp(exception);
@@ -1,3 +1,4 @@
1
+ import { isUuid, } from "@neta-art/cohub";
1
2
  import { createClient } from "../client.js";
2
3
  import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
3
4
  const RESOURCE_TYPES = new Set([
@@ -22,7 +23,6 @@ const REFERENCE_KINDS = new Set([
22
23
  ]);
23
24
  const DIRECTIONS = new Set(["out", "in", "both"]);
24
25
  const GROUP_BYS = new Set(["kind", "targetType", "target", "sourceType", "day"]);
25
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
26
26
  function parseSource(value) {
27
27
  const idx = value.indexOf(":");
28
28
  if (idx <= 0)
@@ -146,7 +146,7 @@ Examples:
146
146
  .action(async (spaceId, opts) => {
147
147
  const client = createClient();
148
148
  try {
149
- if (!UUID_PATTERN.test(spaceId.trim()))
149
+ if (!isUuid(spaceId.trim()))
150
150
  return error("Invalid space id");
151
151
  const groupBy = (opts.groupBy ?? "kind");
152
152
  if (!GROUP_BYS.has(groupBy))
@@ -1,10 +1,10 @@
1
+ import { isUuid } from "@neta-art/cohub";
1
2
  import { createClient } from "../client.js";
2
3
  import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
3
4
  const DEFAULT_LIMIT = 20;
4
5
  const MAX_TITLE_LENGTH = 72;
5
6
  const MAX_CONTEXT_LENGTH = 42;
6
7
  const SEARCH_TYPES = new Set(["turn", "session", "space", "label"]);
7
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
8
8
  function clampLimit(value) {
9
9
  const parsed = Number(value ?? DEFAULT_LIMIT);
10
10
  if (!Number.isFinite(parsed))
@@ -40,7 +40,7 @@ function parseTypes(value) {
40
40
  function parseSearchInput(opts) {
41
41
  const types = parseTypes(opts.types);
42
42
  const spaceId = opts.spaceId?.trim();
43
- if (spaceId && !UUID_PATTERN.test(spaceId))
43
+ if (spaceId && !isUuid(spaceId))
44
44
  throw new Error("Invalid space id");
45
45
  return { types, spaceId: spaceId || undefined, labelRef: opts.labelRef?.trim() || undefined };
46
46
  }
@@ -87,6 +87,15 @@ function printWorkUrls(result) {
87
87
  if (lines.length)
88
88
  console.log(`\n${lines.join("\n")}`);
89
89
  }
90
+ function promotionUrl(publicUrl, promotion) {
91
+ if (!publicUrl)
92
+ return null;
93
+ const url = new URL(publicUrl);
94
+ url.searchParams.set("cohub_campaign", promotion.id);
95
+ for (const [key, value] of Object.entries(promotion.parameters))
96
+ url.searchParams.set(key, value);
97
+ return url.toString();
98
+ }
90
99
  function printWorkStats(stats) {
91
100
  table([stats.summary], [
92
101
  { key: "totalViews", label: "Total" },
@@ -424,6 +433,111 @@ export function registerWorks(program) {
424
433
  handleHttp(e);
425
434
  }
426
435
  });
436
+ const promotionsCmd = worksCmd.command("promotions").description("Work promotion links and analytics");
437
+ promotionsCmd
438
+ .command("list <work>")
439
+ .alias("ls")
440
+ .description("List promotion links")
441
+ .option("--json", "Output as JSON")
442
+ .action(async (workRef, opts) => {
443
+ const client = createClient();
444
+ try {
445
+ const detail = await getWorkByRef(client, workRef);
446
+ const result = await client.works.listPromotions(detail.work.id);
447
+ const promotions = result.promotions.map((promotion) => ({
448
+ ...promotion,
449
+ url: promotionUrl(detail.publicUrl, promotion),
450
+ }));
451
+ if (jsonRequested(opts))
452
+ return outJson({ ...result, promotions });
453
+ table(promotions, [
454
+ { key: "name", label: "Name" },
455
+ { key: "provider", label: "Provider" },
456
+ { key: "id", label: "ID" },
457
+ { key: "url", label: "URL" },
458
+ ]);
459
+ }
460
+ catch (e) {
461
+ handleHttp(e);
462
+ }
463
+ });
464
+ promotionsCmd
465
+ .command("create <work>")
466
+ .description("Create an immutable promotion link")
467
+ .requiredOption("--name <name>", "Promotion name")
468
+ .option("--provider <provider>", "Promotion provider", "generic")
469
+ .option("--utm-id <value>", "UTM campaign ID")
470
+ .option("--utm-source <value>", "UTM source")
471
+ .option("--utm-medium <value>", "UTM medium")
472
+ .option("--utm-campaign <value>", "UTM campaign")
473
+ .option("--utm-term <value>", "UTM term")
474
+ .option("--utm-content <value>", "UTM content")
475
+ .option("--json", "Output as JSON")
476
+ .action(async (workRef, opts) => {
477
+ const client = createClient();
478
+ try {
479
+ const detail = await getWorkByRef(client, workRef);
480
+ const parameters = Object.fromEntries(Object.entries({
481
+ utm_id: opts.utmId,
482
+ utm_source: opts.utmSource,
483
+ utm_medium: opts.utmMedium,
484
+ utm_campaign: opts.utmCampaign,
485
+ utm_term: opts.utmTerm,
486
+ utm_content: opts.utmContent,
487
+ }).filter((entry) => typeof entry[1] === "string"));
488
+ const result = await client.works.createPromotion(detail.work.id, {
489
+ name: opts.name,
490
+ provider: opts.provider,
491
+ parameters,
492
+ });
493
+ const url = promotionUrl(detail.publicUrl, result.promotion);
494
+ if (jsonRequested(opts))
495
+ return outJson({ ...result, url });
496
+ ok("Promotion created");
497
+ table([{ ...result.promotion, url }], [
498
+ { key: "name", label: "Name" },
499
+ { key: "provider", label: "Provider" },
500
+ { key: "id", label: "ID" },
501
+ { key: "url", label: "URL" },
502
+ ]);
503
+ }
504
+ catch (e) {
505
+ handleHttp(e);
506
+ }
507
+ });
508
+ promotionsCmd
509
+ .command("stats <work> <promotionId>")
510
+ .description("Show promotion analytics")
511
+ .option("--json", "Output as JSON")
512
+ .action(async (workRef, promotionId, opts) => {
513
+ const client = createClient();
514
+ try {
515
+ const detail = await getWorkByRef(client, workRef);
516
+ const result = await client.works.getPromotionStats(detail.work.id, promotionId);
517
+ if (jsonRequested(opts))
518
+ return outJson(result);
519
+ table([{
520
+ name: result.promotion.name,
521
+ landing: result.summary.landing,
522
+ ready: result.summary.ready,
523
+ registered: result.summary.registrationCompleted,
524
+ paywall: result.summary.paywallViewed,
525
+ checkout: result.summary.checkoutStarted,
526
+ readyRate: `${(result.summary.readyRate * 100).toFixed(1)}%`,
527
+ }], [
528
+ { key: "name", label: "Name" },
529
+ { key: "landing", label: "Landing" },
530
+ { key: "ready", label: "Ready" },
531
+ { key: "registered", label: "Registered" },
532
+ { key: "paywall", label: "Paywall" },
533
+ { key: "checkout", label: "Checkout" },
534
+ { key: "readyRate", label: "Ready rate" },
535
+ ]);
536
+ }
537
+ catch (e) {
538
+ handleHttp(e);
539
+ }
540
+ });
427
541
  registerWorkCommerce(worksCmd);
428
542
  worksCmd
429
543
  .command("rm <id>")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "3.10.0",
3
+ "version": "3.10.2",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -19,7 +19,7 @@
19
19
  "commander": "^15.0.0",
20
20
  "pixi.js": "^8.19.0",
21
21
  "sharp": "^0.35.3",
22
- "@neta-art/cohub": "5.8.0"
22
+ "@neta-art/cohub": "5.8.2"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"