@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 +1 -1
- package/skills/hyperdecks/SKILL.md +27 -0
- package/src/commands.mjs +39 -1
- package/src/ops.mjs +15 -0
- package/src/runtime.mjs +4 -1
package/package.json
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
},
|