@hypersocial/decks-cli 0.3.0 → 0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypersocial/decks-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Command-line client for Hypersocial Decks live analytics and share management.",
5
5
  "type": "module",
6
6
  "main": "./src/commands.mjs",
@@ -95,6 +95,7 @@ Hyperdecks server; use the CLI where you have a shell and the tool otherwise.
95
95
  | One more link | `hyperdecks share new <slug> "<name>"` | `create_share` |
96
96
  | Who watched | `hyperdecks deck stats <slug>` | `deck_stats` |
97
97
  | All activity | `hyperdecks activity --deck <slug>` | `activity_overview` |
98
+ | What one person watched | `hyperdecks engagement "<name>" --deck <slug> --json` | `recipient_engagement` |
98
99
 
99
100
  ### 1. Ground it in the user's own material
100
101
 
@@ -471,6 +472,32 @@ hyperdecks activity --deck <slug> --limit 10
471
472
  Tell the user who opened it, how far they got and whether the narration
472
473
  finished. QA links never count.
473
474
 
475
+ ### 9. Follow up with a recipient
476
+
477
+ When the user wants to follow up with someone, read what that person watched:
478
+
479
+ ```bash
480
+ hyperdecks engagement "<name>" --deck <slug> --json # MCP: recipient_engagement
481
+ ```
482
+
483
+ Then draft the email:
484
+
485
+ 1. Lead with the slide they spent most time on (`summary.mostTime`) or
486
+ replayed (`replayedSlides`), and answer what that slide raises. Each
487
+ slide's `title` says what it is about.
488
+ 2. If they dropped off (`droppedOffOn`), offer the rest briefly, or a
489
+ 2-minute call, rather than asking them to rewatch.
490
+ 3. If they never opened it (`summary.visits` is 0), write a short nudge
491
+ instead.
492
+
493
+ Use what you know beyond the data: the email thread, who the person is, what
494
+ was promised, and the user's voice. Never tell the recipient they were
495
+ tracked: no "I saw you watched slide 7 three times", no percentages. The
496
+ signal decides what to talk about; it is never something to quote. Put the
497
+ draft in the user's mail tool (or show the text) and never send it without the
498
+ user's go-ahead. `null` means a field was not recorded for that visit (older
499
+ links), not zero.
500
+
474
501
  ### Hosted source (chat agents and the connector)
475
502
 
476
503
  A deck can also live on Hyperdecks with no folder: chat agents build it through
package/src/commands.mjs CHANGED
@@ -12,7 +12,7 @@ import * as ops from "./ops.mjs";
12
12
 
13
13
  export const playbackLabel = (p) => p.narrationFinished ? "narration finished"
14
14
  : p.narrationStarted ? "narration started"
15
- : p.telemetryVersion === 2 ? "narration not started" : "playback unknown (legacy)";
15
+ : (p.telemetryVersion ?? 0) >= 2 ? "narration not started" : "playback unknown (legacy)";
16
16
 
17
17
  const fmtMs = (ms) => (ms >= 60000 ? `${(ms / 60000).toFixed(1)}m` : `${Math.round(ms / 1000)}s`);
18
18
 
@@ -167,6 +167,41 @@ async function activity(pos, opts) {
167
167
  }
168
168
  }
169
169
 
170
+ const pct = (n) => (n === null || n === undefined ? "n/a" : `${n}%`);
171
+ const slideName = (s) => (s.title ? `${s.index} ${s.title}` : `slide ${s.index}`);
172
+
173
+ async function engagement(pos, opts) {
174
+ const who = pos.join(" ");
175
+ if (!who) throw new Error("usage: engagement <recipient|token> [--deck <slug>] [--json]");
176
+ const entries = await ops.recipientEngagement(opts, who, opts.deck);
177
+ if (opts.json) {
178
+ console.log(JSON.stringify(entries, null, 2));
179
+ return;
180
+ }
181
+ if (!entries.length) {
182
+ console.log(`(no links for "${who}"${opts.deck ? ` on ${opts.deck}` : ""})`);
183
+ return;
184
+ }
185
+ for (const e of entries) {
186
+ const s = e.summary;
187
+ console.log(`${e.recipient} — ${e.deck.title} (${e.deck.slug}, link ${e.link.token}${e.link.enabled ? "" : ", off"})`);
188
+ if (!s.visits) {
189
+ console.log(" never opened");
190
+ continue;
191
+ }
192
+ const titles = new Map(e.slides.map((r) => [r.index, r]));
193
+ const list = (xs) => (xs === null ? "n/a" : xs.length ? xs.map((i) => slideName(titles.get(i) ?? { index: i })).join(", ") : "none");
194
+ console.log(` visits: ${s.visits} (last ${new Date(s.lastSeenAt).toISOString().slice(0, 16).replace("T", " ")}) · ${fmtMs(s.totalMs)} engaged`);
195
+ console.log(` narration: ${s.narration.finished ? "finished" : s.narration.started ? "started" : "not started"} · heard ${pct(s.narration.heardPct)}${s.narration.telemetry === "v3" ? "" : " (playback detail unavailable)"}`);
196
+ console.log(` reached: slide ${s.furthestSlide}${e.deck.slides ? ` of ${e.deck.slides}` : ""}${s.droppedOffOn ? ` · dropped off on ${slideName(s.droppedOffOn)}` : ""}`);
197
+ console.log(` replayed: ${list(s.replayedSlides)}`);
198
+ console.log(` skipped: ${list(s.skippedSlides)}`);
199
+ console.log(` most time: ${s.mostTime.map((m) => `${slideName(m)} (${fmtMs(m.ms)})`).join(", ") || "none"}`);
200
+ if (!s.slideData) console.log(" slide data unavailable (this deck reports time only)");
201
+ if (s.deckChangedSinceView) console.log(" the deck changed since their last visit");
202
+ }
203
+ }
204
+
170
205
  async function credits(pos, opts) {
171
206
  const bal = await ops.creditsBalance(opts);
172
207
  const ledger = await ops.creditsLedger(opts, opts.limit || 10);
@@ -272,6 +307,8 @@ commands:
272
307
  skills install [--agent codex,claude-code] [--all-agents]
273
308
  default: every agent found in your home directory (else codex, claude-code)
274
309
  activity [--deck <slug>] [--limit <n>] [--json]
310
+ engagement <recipient|token> [--deck <slug>] [--json]
311
+ what one recipient watched per deck: narration heard, replays, drop-off, visits
275
312
  gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--transparent] [--image <url>] [--size <s>]
276
313
  [--quality <q>] [--count <n>] [--seed <n>] [--slug <deck>] [--download <file>]
277
314
  default model gpt-image-2.5; --seed applies to nano-banana-2.1
@@ -653,6 +690,7 @@ function commandTable(pos, rest, opts) {
653
690
  "skill install": () => installHyperdecksSkill(opts, rest),
654
691
  "skills install": () => installHyperdecksSkill(opts, rest),
655
692
  activity: () => activity(pos.slice(1), opts),
693
+ engagement: () => engagement(pos.slice(1), opts),
656
694
  "gen-image": () => genImage(pos.slice(1), opts),
657
695
  "image-job": () => imageJob(pos.slice(1), opts),
658
696
  "gen-video": () => genVideo(pos.slice(1), opts),
package/src/ops.mjs CHANGED
@@ -36,6 +36,21 @@ export async function activityOverview(opts = {}) {
36
36
  return (await client(opts)).query(api.activity.overview, {});
37
37
  }
38
38
 
39
+ // What one recipient watched, per deck link (or one link by token): the data
40
+ // the owner's agent drafts a follow-up from. A 16-character link token is
41
+ // looked up as a token; anything else is a recipient name.
42
+ export const RECIPIENT_ENGAGEMENT_DESCRIPTION =
43
+ "What one recipient watched, per deck link: visits, narration heard (%), replayed / skipped slides, " +
44
+ "where they dropped off, time per slide, each visit, and forwards. Use it to draft a follow-up: lead " +
45
+ "with what they spent time on, and never tell the recipient they were tracked.";
46
+ export const SHARE_TOKEN = /^[a-f0-9]{16}$/;
47
+ export async function recipientEngagement(opts, who, deck) {
48
+ const value = String(who ?? "").trim();
49
+ if (!value) throw new Error("a recipient name or share token is required");
50
+ const args = SHARE_TOKEN.test(value) ? { token: value } : { recipient: value, ...(deck ? { deckSlug: String(deck) } : {}) };
51
+ return (await client(opts)).query(api.engagement.recipientEngagement, args);
52
+ }
53
+
39
54
  // Generate an image via fal (GPT Image 2.5 by default, or Nano Banana 2.1 / GPT
40
55
  // Image 2 with `model`; t2i/i2i, optional BiRefNet transparency),
41
56
  // store it in Convex, and return the hosted URL. Runs the imagegen action as the
package/src/runtime.mjs CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import { ConvexHttpClient } from "convex/browser";
9
9
  import { makeFunctionReference } from "convex/server";
10
10
 
11
- export const VERSION = "0.3.0";
11
+ export const VERSION = "0.3.1";
12
12
  const UPLOAD_TIMEOUT_MS = 125_000;
13
13
 
14
14
  export const PROD = {
@@ -38,6 +38,9 @@ export const api = {
38
38
  overview: makeFunctionReference("activity:overview"),
39
39
  setArchived: makeFunctionReference("activity:setArchived"),
40
40
  },
41
+ engagement: {
42
+ recipientEngagement: makeFunctionReference("engagement:recipientEngagement"),
43
+ },
41
44
  cliCredentials: {
42
45
  revokeByHash: makeFunctionReference("cliCredentials:revokeByHash"),
43
46
  },