@cloudglue/tinycloud 0.3.0

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.
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+
3
+ const { execFileSync } = require("node:child_process");
4
+
5
+ const DEFAULT_BASE = "https://media.cloudglue.dev/tinycloud-dist";
6
+
7
+ // Node's fetch ignores proxy env vars; shell out to curl when one is set so
8
+ // the manifest/sidecar fetches behave like the tarball download (and like
9
+ // install.sh, which always uses curl).
10
+ function useCurl() {
11
+ return !!(process.env.HTTPS_PROXY || process.env.https_proxy || process.env.TINYCLOUD_USE_CURL === "1");
12
+ }
13
+
14
+ /** GET url → {status, text}. status 0 = network failure. Proxy-aware. */
15
+ async function httpGetText(url) {
16
+ if (useCurl()) {
17
+ try {
18
+ const out = execFileSync("curl", ["-sSL", "-w", "\n%{http_code}", url], { encoding: "utf8" });
19
+ const i = out.lastIndexOf("\n");
20
+ return { status: Number(out.slice(i + 1).trim()) || 0, text: out.slice(0, i) };
21
+ } catch {
22
+ return { status: 0, text: "" };
23
+ }
24
+ }
25
+ try {
26
+ const res = await fetch(url);
27
+ return { status: res.status, text: res.ok ? await res.text() : "" };
28
+ } catch {
29
+ return { status: 0, text: "" };
30
+ }
31
+ }
32
+
33
+ /** HEAD url → ETag header value or null. Proxy-aware. */
34
+ async function httpHeadEtag(url) {
35
+ if (useCurl()) {
36
+ try {
37
+ const out = execFileSync("curl", ["-sSIL", url], { encoding: "utf8" });
38
+ const m = out.match(/^etag:\s*(.+)$/im);
39
+ return m ? m[1].trim() : null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ try {
45
+ const res = await fetch(url, { method: "HEAD" });
46
+ return res.ok ? res.headers.get("etag") : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function baseUrl() {
53
+ return (process.env.TINYCLOUD_DIST_URL || DEFAULT_BASE).replace(/\/+$/, "");
54
+ }
55
+
56
+ function requireManifest() {
57
+ return process.env.TINYCLOUD_REQUIRE_MANIFEST === "1";
58
+ }
59
+
60
+ // Pinned tarballs on the CDN are v-prefixed (tinycloud-<platform>-v0.3.0.tar.gz);
61
+ // version strings stay bare everywhere else.
62
+ function tarballName(target, version) {
63
+ return version ? `tinycloud-${target}-v${version}.tar.gz` : `tinycloud-${target}.tar.gz`;
64
+ }
65
+
66
+ /**
67
+ * The manifest is an optimization, never a requirement (unless strict mode):
68
+ * a missing, erroring, or unusable manifest (CloudFront 403-for-missing,
69
+ * 5xx, captive-portal HTML, future schema) degrades to null so commands
70
+ * that don't need it keep working. Checksum MISMATCHES still always fail.
71
+ */
72
+ async function fetchManifest() {
73
+ const url = `${baseUrl()}/manifest.json`;
74
+ const degrade = (reason) => {
75
+ if (requireManifest()) {
76
+ throw new Error(`TINYCLOUD_REQUIRE_MANIFEST=1 but the release manifest is unavailable: ${reason}`);
77
+ }
78
+ return null;
79
+ };
80
+ const { status, text } = await httpGetText(url);
81
+ if (status === 0 || status === 403 || status === 404) {
82
+ return degrade(`${url} is missing (HTTP ${status || "network error"})`);
83
+ }
84
+ if (status !== 200) {
85
+ process.stderr.write(`tinycloud: fetching ${url} failed (HTTP ${status}); proceeding without it\n`);
86
+ return degrade(`HTTP ${status}`);
87
+ }
88
+ let manifest;
89
+ try {
90
+ manifest = JSON.parse(text);
91
+ } catch {
92
+ process.stderr.write(`tinycloud: ${url} is not valid JSON; proceeding without it\n`);
93
+ return degrade("invalid JSON");
94
+ }
95
+ if (manifest.schema !== 1) {
96
+ process.stderr.write(
97
+ `tinycloud: unsupported manifest schema ${manifest.schema} at ${url}; proceeding without it (upgrade @cloudglue/tinycloud)\n`
98
+ );
99
+ return degrade(`unsupported schema ${manifest.schema}`);
100
+ }
101
+ return manifest;
102
+ }
103
+
104
+ /** Try the <tarball>.sha256 sidecar; returns the hex hash or null. */
105
+ async function fetchSidecarSha256(tarballUrl) {
106
+ const { status, text } = await httpGetText(`${tarballUrl}.sha256`);
107
+ if (status !== 200) return null;
108
+ const match = text.trim().match(/^[0-9a-f]{64}/i);
109
+ return match ? match[0].toLowerCase() : null;
110
+ }
111
+
112
+ /**
113
+ * Resolve a version request to a concrete download.
114
+ * @param {string} versionOrLatest exact semver, or "latest"
115
+ * @param {string} channel "stable" | "beta"
116
+ * @param {string} target platform key like "darwin-arm64"
117
+ * @returns {{version: string|null, url: string, sha256: string|null, size: number|null, verified: boolean}}
118
+ */
119
+ async function resolveDownload(versionOrLatest, channel, target, prefetchedManifest) {
120
+ // Callers that already fetched the manifest (update / install --latest)
121
+ // pass it in to avoid a second network round-trip for the same JSON.
122
+ const manifest = prefetchedManifest !== undefined ? prefetchedManifest : await fetchManifest();
123
+ const base = baseUrl();
124
+
125
+ if (manifest) {
126
+ let version = versionOrLatest;
127
+ const userPinned = versionOrLatest !== "latest";
128
+ if (versionOrLatest === "latest") {
129
+ version = manifest.channels && manifest.channels[channel];
130
+ if (!version) throw new Error(`Channel "${channel}" has no released version in the manifest`);
131
+ // Manifest-resolved versions get the same leading-v normalization as
132
+ // user input (install.sh does the same with ${VERSION#v})
133
+ version = String(version).replace(/^v/, "");
134
+ }
135
+ const entry = manifest.versions && manifest.versions[version];
136
+ const plat = entry && entry.platforms && entry.platforms[target];
137
+ if (plat) {
138
+ // An explicit distribution base (mirror, fixture) wins over the
139
+ // manifest's absolute URLs — otherwise the override only redirects
140
+ // the manifest fetch while tarballs still hit the canonical CDN.
141
+ const url = process.env.TINYCLOUD_DIST_URL ? `${base}/${plat.url.split("/").pop()}` : plat.url;
142
+ return { version, url, sha256: plat.sha256 || null, size: plat.size || null, verified: !!plat.sha256 };
143
+ }
144
+ if (!userPinned || requireManifest()) {
145
+ throw new Error(
146
+ entry ? `Version ${version} has no build for ${target}` : `Version ${version} not found in the release manifest`
147
+ );
148
+ }
149
+ // A user-pinned version missing from the manifest (e.g. a pre-manifest
150
+ // release whose tarball is still on the CDN) falls back to the
151
+ // conventional URL + sidecar instead of hard-failing.
152
+ process.stderr.write(`tinycloud: version ${version} is not in the release manifest; trying the direct URL\n`);
153
+ const url = `${base}/${tarballName(target, version)}`;
154
+ const sha256 = await fetchSidecarSha256(url);
155
+ return { version, url, sha256, size: null, verified: !!sha256 };
156
+ }
157
+
158
+ // No manifest published: fall back to direct tarball URLs.
159
+ if (channel !== "stable") {
160
+ throw new Error(`Channel "${channel}" requires the release manifest, which is not available`);
161
+ }
162
+ const version = versionOrLatest === "latest" ? null : versionOrLatest;
163
+ const url = `${base}/${tarballName(target, version)}`;
164
+ const sha256 = await fetchSidecarSha256(url);
165
+ if (!sha256) {
166
+ process.stderr.write(
167
+ "tinycloud: release manifest and checksum sidecar not found — proceeding without checksum verification\n"
168
+ );
169
+ }
170
+ return { version, url, sha256, size: null, verified: !!sha256 };
171
+ }
172
+
173
+ module.exports = {
174
+ baseUrl,
175
+ fetchManifest,
176
+ fetchSidecarSha256,
177
+ resolveDownload,
178
+ tarballName,
179
+ httpHeadEtag,
180
+ DEFAULT_BASE,
181
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ const SUPPORTED = new Set(["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"]);
4
+
5
+ class PlatformError extends Error {}
6
+
7
+ /** Resolve the current platform to a distribution target like "darwin-arm64". */
8
+ function resolveTarget(platform = process.platform, arch = process.arch) {
9
+ if (platform === "win32") {
10
+ throw new PlatformError(
11
+ "Windows is not supported. Use WSL2 (https://learn.microsoft.com/windows/wsl/) and re-run inside your Linux distro."
12
+ );
13
+ }
14
+ const normArch = arch === "x64" || arch === "amd64" ? "x64" : arch === "aarch64" ? "arm64" : arch;
15
+ const key = `${platform}-${normArch}`;
16
+ if (!SUPPORTED.has(key)) {
17
+ throw new PlatformError(`Unsupported platform: ${key}. Supported: ${[...SUPPORTED].join(", ")}`);
18
+ }
19
+ return key;
20
+ }
21
+
22
+ module.exports = { resolveTarget, SUPPORTED, PlatformError };
package/lib/run.js ADDED
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+
3
+ const path = require("node:path");
4
+ const { spawn } = require("node:child_process");
5
+
6
+ /**
7
+ * Exec passthrough to the real binary: inherit stdio, forward signals, and
8
+ * preserve exit semantics (including 128+n signal death).
9
+ */
10
+ function run(binPath, args, installDir) {
11
+ const env = {
12
+ ...process.env,
13
+ PATH: `${path.join(installDir, "bin")}${path.delimiter}${process.env.PATH || ""}`,
14
+ };
15
+ const child = spawn(binPath, args, { stdio: "inherit", env });
16
+ const forwarders = new Map();
17
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
18
+ const forward = () => {
19
+ try {
20
+ child.kill(sig);
21
+ } catch {}
22
+ };
23
+ forwarders.set(sig, forward);
24
+ process.on(sig, forward);
25
+ }
26
+ child.on("error", (err) => {
27
+ process.stderr.write(`tinycloud: failed to launch binary: ${err.message}\n`);
28
+ process.exit(1);
29
+ });
30
+ child.on("close", (code, signal) => {
31
+ if (signal) {
32
+ // Remove only our forwarder (not listeners owned by parent tooling),
33
+ // then re-raise so our exit status preserves 128+n semantics.
34
+ const forward = forwarders.get(signal);
35
+ if (forward) process.removeListener(signal, forward);
36
+ process.kill(process.pid, signal);
37
+ return;
38
+ }
39
+ process.exit(code == null ? 1 : code);
40
+ });
41
+ }
42
+
43
+ module.exports = { run };
package/lib/skills.js ADDED
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+
7
+ // Agent skills bundled with this package (the npm tarball includes skills/).
8
+ function bundledSkillsDir() {
9
+ return path.join(__dirname, "..", "skills");
10
+ }
11
+
12
+ function listBundledSkills() {
13
+ const dir = bundledSkillsDir();
14
+ if (!fs.existsSync(dir)) return [];
15
+ return fs
16
+ .readdirSync(dir)
17
+ .filter((e) => fs.existsSync(path.join(dir, e, "SKILL.md")))
18
+ .sort();
19
+ }
20
+
21
+ /**
22
+ * Pick install targets. Each harness keeps skills in its own directory; we
23
+ * detect harnesses by their config dir to avoid littering projects that
24
+ * don't use them.
25
+ * claude-code: <project>/.claude/skills (global: ~/.claude/skills)
26
+ * codex: <project>/.agents/skills (agentskills.io layout)
27
+ */
28
+ function resolveTargets({ global: isGlobal, dir, cwd = process.cwd() }) {
29
+ if (dir) return [{ name: "custom", dir: path.resolve(dir) }];
30
+ if (isGlobal) return [{ name: "claude-code (global)", dir: path.join(os.homedir(), ".claude", "skills") }];
31
+
32
+ const targets = [];
33
+ if (fs.existsSync(path.join(cwd, ".claude"))) {
34
+ targets.push({ name: "claude-code", dir: path.join(cwd, ".claude", "skills") });
35
+ }
36
+ if (fs.existsSync(path.join(cwd, ".agents"))) {
37
+ targets.push({ name: "codex", dir: path.join(cwd, ".agents", "skills") });
38
+ }
39
+ if (targets.length === 0) {
40
+ // No harness detected: default to claude-code project layout.
41
+ targets.push({ name: "claude-code", dir: path.join(cwd, ".claude", "skills") });
42
+ }
43
+ return targets;
44
+ }
45
+
46
+ function installSkills({ skills, targets, force }) {
47
+ const src = bundledSkillsDir();
48
+ const results = [];
49
+ for (const target of targets) {
50
+ fs.mkdirSync(target.dir, { recursive: true });
51
+ for (const skill of skills) {
52
+ const from = path.join(src, skill);
53
+ const to = path.join(target.dir, skill);
54
+ if (fs.existsSync(to) && !force) {
55
+ results.push({ target: target.name, skill, dir: to, status: "skipped (exists; use --force)" });
56
+ continue;
57
+ }
58
+ fs.rmSync(to, { recursive: true, force: true });
59
+ fs.cpSync(from, to, { recursive: true });
60
+ results.push({ target: target.name, skill, dir: to, status: "installed" });
61
+ }
62
+ }
63
+ return results;
64
+ }
65
+
66
+ const USAGE = `Usage: tinycloud skills <list|install> [options]
67
+
68
+ list List the agent skills bundled with this package
69
+ install Copy skills into your agent's skills directory
70
+
71
+ Install options:
72
+ --skill <a,b,...> Only these skills (default: all)
73
+ --global Install to ~/.claude/skills instead of the project
74
+ --dir <path> Install to an explicit directory
75
+ --force Overwrite skills that are already installed
76
+
77
+ Detection: a project .claude/ dir targets Claude Code (.claude/skills),
78
+ a .agents/ dir targets Codex (.agents/skills); both when both exist.
79
+ `;
80
+
81
+ async function cmdSkills(args) {
82
+ const action = args[0];
83
+ const available = listBundledSkills();
84
+
85
+ if (!action || action === "--help" || action === "-h") {
86
+ process.stdout.write(USAGE);
87
+ return;
88
+ }
89
+ if (action === "list") {
90
+ for (const s of available) console.log(s);
91
+ return;
92
+ }
93
+ if (action !== "install") {
94
+ throw new Error(`Unknown skills action: ${action}\n${USAGE}`);
95
+ }
96
+
97
+ let wanted = available;
98
+ let isGlobal = false;
99
+ let force = false;
100
+ let dir;
101
+ for (let i = 1; i < args.length; i++) {
102
+ if (args[i] === "--skill" && args[i + 1]) {
103
+ const names = args[++i].split(",").map((s) => s.trim()).filter(Boolean);
104
+ const unknown = names.filter((n) => !available.includes(n));
105
+ if (unknown.length) {
106
+ throw new Error(`Unknown skill(s): ${unknown.join(", ")}. Available: ${available.join(", ")}`);
107
+ }
108
+ wanted = names;
109
+ } else if (args[i] === "--global") isGlobal = true;
110
+ else if (args[i] === "--force") force = true;
111
+ else if (args[i] === "--dir" && args[i + 1]) dir = args[++i];
112
+ else throw new Error(`Unknown install option: ${args[i]}\n${USAGE}`);
113
+ }
114
+
115
+ if (available.length === 0) {
116
+ throw new Error("No bundled skills found in this package installation");
117
+ }
118
+
119
+ const targets = resolveTargets({ global: isGlobal, dir });
120
+ const results = installSkills({ skills: wanted, targets, force });
121
+ for (const r of results) console.log(`${r.status === "installed" ? "✓" : "-"} ${r.skill} → ${r.dir} [${r.status}]`);
122
+ const installed = results.filter((r) => r.status === "installed").length;
123
+ console.log(`\n${installed} skill(s) installed${installed ? ". Restart your agent session to pick them up." : "."}`);
124
+ }
125
+
126
+ module.exports = { cmdSkills, resolveTargets, installSkills, listBundledSkills, bundledSkillsDir };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@cloudglue/tinycloud",
3
+ "version": "0.3.0",
4
+ "description": "Agent CLI for deep video work, by Cloudglue. Downloads the tinycloud binary on first run.",
5
+ "bin": {
6
+ "tinycloud": "bin/tinycloud.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "lib/",
11
+ "skills/",
12
+ "LICENSE.md",
13
+ "THIRD_PARTY_NOTICES.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "scripts": {
19
+ "test": "node --test test/*.test.mjs"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/cloudglue/tinycloud.git"
24
+ },
25
+ "homepage": "https://tinycloud.sh",
26
+ "keywords": ["video", "cloudglue", "cli", "agent", "captions", "clips"],
27
+ "license": "SEE LICENSE IN LICENSE.md"
28
+ }
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: ad-analysis
3
+ description: >-
4
+ Analyze a video ad into an HTML breakdown with shot timeline, hook
5
+ classification, pacing, structure, CTA, and takeaways. Use when the user
6
+ wants ad creative analysis, competitive ad research, or a hook/pacing/CTA
7
+ breakdown of a commercial or social ad. Takes one source: a local video
8
+ file, URL, or cloudglue:// file URI (e.g. cloudglue://files/<id>). Runs the built-in tinycloud "ad-analysis"
9
+ workflow; requires the tinycloud CLI configured with a Cloudglue API key
10
+ (analysis runs through the user's Cloudglue account).
11
+ argument-hint: "[ad video file, URL, or cloudglue:// file URI]"
12
+ arguments: source
13
+ ---
14
+
15
+ # Video-ad analysis
16
+
17
+ This skill is a thin wrapper around the `ad-analysis` workflow recipe bundled
18
+ inside the tinycloud binary (`watch → extract → render`).
19
+
20
+ ## Run
21
+
22
+ 1. **Check the CLI.** If the general `tinycloud` skill is installed alongside
23
+ this one, run its `scripts/preflight.sh`. Otherwise verify directly:
24
+
25
+ ```bash
26
+ tinycloud setup --check --json # ready when data.ok == true
27
+ ```
28
+
29
+ Missing CLI: `npm install -g @cloudglue/tinycloud` (see https://tinycloud.sh).
30
+ Missing key: `tinycloud setup cloudglue --api-key <key>`.
31
+
32
+ 2. **Confirm the recipe is available** (free, no cloud calls):
33
+
34
+ ```bash
35
+ tinycloud workflow validate ad-analysis --json
36
+ ```
37
+
38
+ 3. **Run it** with the user's source. The analysis steps run through the
39
+ configured Cloudglue API key — if the user has not clearly asked to run
40
+ it, show the step plan first with
41
+ `tinycloud workflow plan ad-analysis $source --json` (free).
42
+
43
+ ```bash
44
+ tinycloud workflow ad-analysis $source --json
45
+ ```
46
+
47
+ Useful params: `--param segment=shots` (default; shot-level timeline the
48
+ breakdown is built around) or `--param segment=uniform:20` for a lighter
49
+ uniform pass; `--param out=<path>` to control the HTML location.
50
+
51
+ ## Read the result
52
+
53
+ Parse the single JSON envelope from stdout (machine output; logs are stderr):
54
+
55
+ - Success: `status == "ready"` and `data.status == "completed"`.
56
+ - The analysis path is `data.outputs.html` (also in `data.artifacts[]`).
57
+ Default: `./tinycloud-output/runs/<data.run_id>/ad-analysis.html`.
58
+ - Report the HTML path; offer
59
+ `tinycloud publish <html> --name ad-analysis --visibility private --json`
60
+ to host it as a shareable page.
61
+
62
+ Any other `status` (`needs_credentials`, `needs_upload`, `pending`, `paused`,
63
+ `error`) or `data.status` of `partial`/`failed`: stop, report the envelope's
64
+ `error.message`, and follow its `setup` / `resume` / `next` hints. The general
65
+ `tinycloud` skill (if installed) documents full status handling.
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: blog-post
3
+ description: >-
4
+ Transform a video into a rich blog post (HTML + embedded markdown) with
5
+ sections, thumbnails, and key takeaways. Use when the user wants to turn a
6
+ video, talk, demo, or tutorial into a written article or blog content.
7
+ Takes one source: a local video file, URL, or cloudglue:// file URI (e.g. cloudglue://files/<id>). Runs the
8
+ built-in tinycloud "blog-post" workflow; requires the tinycloud CLI
9
+ configured with a Cloudglue API key (analysis runs through the user's
10
+ Cloudglue account).
11
+ argument-hint: "[video file, URL, or cloudglue:// file URI]"
12
+ arguments: source
13
+ ---
14
+
15
+ # Video → blog post
16
+
17
+ This skill is a thin wrapper around the `blog-post` workflow recipe bundled
18
+ inside the tinycloud binary (`watch → extract → render`).
19
+
20
+ ## Run
21
+
22
+ 1. **Check the CLI.** If the general `tinycloud` skill is installed alongside
23
+ this one, run its `scripts/preflight.sh`. Otherwise verify directly:
24
+
25
+ ```bash
26
+ tinycloud setup --check --json # ready when data.ok == true
27
+ ```
28
+
29
+ Missing CLI: `npm install -g @cloudglue/tinycloud` (see https://tinycloud.sh).
30
+ Missing key: `tinycloud setup cloudglue --api-key <key>`.
31
+
32
+ 2. **Confirm the recipe is available** (free, no cloud calls):
33
+
34
+ ```bash
35
+ tinycloud workflow validate blog-post --json
36
+ ```
37
+
38
+ 3. **Run it** with the user's source. The analysis steps run through the
39
+ configured Cloudglue API key — if the user has not clearly asked to run
40
+ it, show the step plan first with
41
+ `tinycloud workflow plan blog-post $source --json` (free).
42
+
43
+ ```bash
44
+ tinycloud workflow blog-post $source --json
45
+ ```
46
+
47
+ Useful params: `--param segment=chapters` (default; semantic section
48
+ anchors) or `--param segment=uniform:20` for a lighter pass;
49
+ `--param out=<path>` to control the HTML location.
50
+
51
+ ## Read the result
52
+
53
+ Parse the single JSON envelope from stdout (machine output; logs are stderr):
54
+
55
+ - Success: `status == "ready"` and `data.status == "completed"`.
56
+ - The article path is `data.outputs.html` (also in `data.artifacts[]`).
57
+ Default: `./tinycloud-output/runs/<data.run_id>/blog-post.html`.
58
+ - Report the HTML path; offer
59
+ `tinycloud publish <html> --name blog-post --visibility private --json`
60
+ to host it as a shareable page.
61
+
62
+ Any other `status` (`needs_credentials`, `needs_upload`, `pending`, `paused`,
63
+ `error`) or `data.status` of `partial`/`failed`: stop, report the envelope's
64
+ `error.message`, and follow its `setup` / `resume` / `next` hints. The general
65
+ `tinycloud` skill (if installed) documents full status handling.
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: meeting-breakdown
3
+ description: >-
4
+ Generate a visual meeting breakdown (HTML) with speaker timeline, topic
5
+ labels, chapter summaries, and action items from a meeting recording. Use
6
+ when the user wants meeting notes, a recap, action items, or a who-said-what
7
+ timeline from a recorded meeting or call. Takes one source: a local video
8
+ file, URL, or cloudglue:// file URI (e.g. cloudglue://files/<id>). Runs the built-in tinycloud
9
+ "meeting-breakdown" workflow; requires the tinycloud CLI configured with a
10
+ Cloudglue API key (analysis runs through the user's Cloudglue account).
11
+ argument-hint: "[meeting recording file, URL, or cloudglue:// file URI]"
12
+ arguments: source
13
+ ---
14
+
15
+ # Meeting breakdown
16
+
17
+ This skill is a thin wrapper around the `meeting-breakdown` workflow recipe
18
+ bundled inside the tinycloud binary (`watch → extract ×2 → render`).
19
+
20
+ ## Run
21
+
22
+ 1. **Check the CLI.** If the general `tinycloud` skill is installed alongside
23
+ this one, run its `scripts/preflight.sh`. Otherwise verify directly:
24
+
25
+ ```bash
26
+ tinycloud setup --check --json # ready when data.ok == true
27
+ ```
28
+
29
+ Missing CLI: `npm install -g @cloudglue/tinycloud` (see https://tinycloud.sh).
30
+ Missing key: `tinycloud setup cloudglue --api-key <key>`.
31
+
32
+ 2. **Confirm the recipe is available** (free, no cloud calls):
33
+
34
+ ```bash
35
+ tinycloud workflow validate meeting-breakdown --json
36
+ ```
37
+
38
+ 3. **Run it** with the user's source. The analysis steps (one describe + two
39
+ extracts) run through the configured Cloudglue API key — if the user has
40
+ not clearly asked to run it, show the step plan first with
41
+ `tinycloud workflow plan meeting-breakdown $source --json` (free).
42
+
43
+ ```bash
44
+ tinycloud workflow meeting-breakdown $source --json
45
+ ```
46
+
47
+ Useful params: `--param segment=chapters` (default; semantic meeting
48
+ narrative) or `--param segment=uniform:20` for dense raw intervals;
49
+ `--param out=<path>` to control the HTML location.
50
+
51
+ ## Read the result
52
+
53
+ Parse the single JSON envelope from stdout (machine output; logs are stderr):
54
+
55
+ - Success: `status == "ready"` and `data.status == "completed"`.
56
+ - The breakdown path is `data.outputs.html` (also in `data.artifacts[]`).
57
+ Default: `./tinycloud-output/runs/<data.run_id>/meeting-breakdown.html`.
58
+ - Report the HTML path; offer
59
+ `tinycloud publish <html> --name meeting-breakdown --visibility private --json`
60
+ to host it as a shareable page.
61
+
62
+ Any other `status` (`needs_credentials`, `needs_upload`, `pending`, `paused`,
63
+ `error`) or `data.status` of `partial`/`failed`: stop, report the envelope's
64
+ `error.message`, and follow its `setup` / `resume` / `next` hints. The general
65
+ `tinycloud` skill (if installed) documents full status handling.
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: sales-coaching
3
+ description: >-
4
+ Turn a sales-call recording into a coaching dashboard (HTML) with call
5
+ scores, speech metrics, objections, and improvement areas. Use when the
6
+ user wants sales-call analysis, call coaching, or rep feedback from a
7
+ video/audio recording. Takes one source: a local video file, URL, or
8
+ cloudglue:// file URI (e.g. cloudglue://files/<id>). Runs the built-in tinycloud "sales-coaching" workflow;
9
+ requires the tinycloud CLI configured with a Cloudglue API key (analysis
10
+ runs through the user's Cloudglue account).
11
+ argument-hint: "[sales call video file, URL, or cloudglue:// file URI]"
12
+ arguments: source
13
+ ---
14
+
15
+ # Sales-call coaching dashboard
16
+
17
+ This skill is a thin wrapper around the `sales-coaching` workflow recipe
18
+ bundled inside the tinycloud binary (`watch → extract ×2 → render`).
19
+
20
+ ## Run
21
+
22
+ 1. **Check the CLI.** If the general `tinycloud` skill is installed alongside
23
+ this one, run its `scripts/preflight.sh`. Otherwise verify directly:
24
+
25
+ ```bash
26
+ tinycloud setup --check --json # ready when data.ok == true
27
+ ```
28
+
29
+ Missing CLI: `npm install -g @cloudglue/tinycloud` (see https://tinycloud.sh).
30
+ Missing key: `tinycloud setup cloudglue --api-key <key>`.
31
+
32
+ 2. **Confirm the recipe is available** (free, no cloud calls):
33
+
34
+ ```bash
35
+ tinycloud workflow validate sales-coaching --json
36
+ ```
37
+
38
+ 3. **Run it** with the user's source. The analysis steps (one describe + two
39
+ extracts) run through the configured Cloudglue API key — if the user has
40
+ not clearly asked to run it, show them the step plan first with
41
+ `tinycloud workflow plan sales-coaching $source --json` (free).
42
+
43
+ ```bash
44
+ tinycloud workflow sales-coaching $source --json
45
+ ```
46
+
47
+ (The recipe self-permits its local render step — no extra flag needed.)
48
+ Useful params: `--param segment=chapters` (default; semantic call phases) or
49
+ `--param segment=uniform:20` for dense fixed intervals;
50
+ `--param out=<path>` to control the HTML location.
51
+
52
+ ## Read the result
53
+
54
+ Parse the single JSON envelope from stdout (machine output; logs are stderr):
55
+
56
+ - Success: `status == "ready"` and `data.status == "completed"`.
57
+ - The dashboard path is `data.outputs.html` (also in `data.artifacts[]`).
58
+ Default: `./tinycloud-output/runs/<data.run_id>/sales-coaching.html`.
59
+ - Report the HTML path to the user; offer
60
+ `tinycloud publish <html> --name sales-coaching --visibility private --json`
61
+ to host it as a shareable page.
62
+
63
+ Any other `status` (`needs_credentials`, `needs_upload`, `pending`, `paused`,
64
+ `error`) or `data.status` of `partial`/`failed`: stop, report the envelope's
65
+ `error.message`, and follow its `setup` / `resume` / `next` hints. The general
66
+ `tinycloud` skill (if installed) documents full status handling.