@tacone/prosey 0.2.0 → 0.2.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.
package/README.md CHANGED
@@ -153,6 +153,18 @@ Transcripts and summaries are cached to `/tmp/prosey/`. Repeated invocations
153
153
  for the same video and options are instant and work offline. Use `--no-cache`
154
154
  to skip cache reads and force a fresh fetch.
155
155
 
156
+ When running `prosey summarize`, the command runs inside the cache directory
157
+ for that video. This prevents the AI agent from picking up project-specific
158
+ files like `AGENTS.md` or `CLAUDE.md` from the current folder, and limits its
159
+ ability to modify files outside that directory.
160
+
161
+ ## Exit codes
162
+
163
+ | Code | Meaning |
164
+ | ---- | ----------------------- |
165
+ | `0` | Success |
166
+ | `65` | Invalid video URL or ID |
167
+
156
168
  ## How it works
157
169
 
158
170
  prosey uses YouTube's Innertube API via the
package/bin/prosey CHANGED
@@ -13851,11 +13851,25 @@ var FALLBACK_CONFIG_TOML = `# Default prosey configuration
13851
13851
  # Prompt sent to the command via stdin.
13852
13852
  # Customize this to change how transcripts are summarized.
13853
13853
  prompt = """
13854
- Summarize the following transcript.
13855
- Focus on the key points and main arguments.
13854
+ Write a comprehensive summary of the following transcription.
13856
13855
  """
13857
13856
 
13858
- # Command to execute with the prompt piped via stdin.
13857
+ # Command to execute with the prompt and transcript piped via stdin.
13858
+ # The transcript is appended to the prompt automatically.
13859
+ #
13860
+ # Available options:
13861
+ #
13862
+ # opencode run — full access (default)
13863
+ # opencode run --permissions read — read-only (view files, no edits)
13864
+ #
13865
+ # claude -p "" --print — full access (--print for clean output)
13866
+ # claude --permission-mode plan -p "" --print — read-only (plan/read only)
13867
+ #
13868
+ # copilot -sp "" — full access (-s = silent, -p = prompt)
13869
+ # copilot -sp "" --deny-all-tools — read-only (no shell/write access)
13870
+ #
13871
+ # codex --sandbox default -p "" — full access
13872
+ # codex --sandbox read-only -p "" — read-only
13859
13873
  command = "opencode run"
13860
13874
  `;
13861
13875
  async function readDefaultConfig() {
@@ -13934,7 +13948,11 @@ async function summarize(options) {
13934
13948
 
13935
13949
  ${transcript}`;
13936
13950
  const output = await executeCommand(command, fullPrompt, cwd);
13937
- return output.replace(fullPrompt, "").replace(/\n+$/, "");
13951
+ const cleaned = output.startsWith(fullPrompt) ? output.slice(fullPrompt.length).replace(/\n+$/, "") : output.replace(/\n+$/, "");
13952
+ if (!cleaned || cleaned === transcript) {
13953
+ throw new Error("Summarization command returned no meaningful output");
13954
+ }
13955
+ return cleaned;
13938
13956
  }
13939
13957
 
13940
13958
  // src/cache.ts
@@ -13942,6 +13960,16 @@ import { createHash } from "node:crypto";
13942
13960
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
13943
13961
  import { existsSync as existsSync2 } from "node:fs";
13944
13962
  import { join as join2 } from "node:path";
13963
+ var RE_YOUTUBE2 = /(?:v=|\/|v\/|embed\/|watch\?.*v=|youtu\.be\/|\/v\/|e\/|watch\?.*vi?=|\/embed\/|\/v\/|vi?\/|watch\?.*vi?=|youtu\.be\/|\/vi?\/|\/e\/)([a-zA-Z0-9_-]{11})/i;
13964
+ var RE_BARE_ID = /^[a-zA-Z0-9_-]{11}$/;
13965
+ function extractVideoId(input) {
13966
+ if (RE_BARE_ID.test(input))
13967
+ return input;
13968
+ const match = input.match(RE_YOUTUBE2);
13969
+ if (match)
13970
+ return match[1] || null;
13971
+ return null;
13972
+ }
13945
13973
  function hashOptions(opts) {
13946
13974
  return createHash("sha256").update(JSON.stringify(opts)).digest("hex").slice(0, 8);
13947
13975
  }
@@ -13964,9 +13992,87 @@ async function writeCache(dir, filename, data) {
13964
13992
  await writeFile2(join2(dir, filename), data, "utf8");
13965
13993
  }
13966
13994
 
13995
+ // src/debug.ts
13996
+ var GRAY = "\x1B[90m";
13997
+ var RESET = "\x1B[0m";
13998
+ var enabled = false;
13999
+ function enableDebug() {
14000
+ enabled = true;
14001
+ }
14002
+ function debug(...args) {
14003
+ if (!enabled)
14004
+ return;
14005
+ console.error(GRAY, ...args, RESET);
14006
+ }
14007
+ // package.json
14008
+ var package_default = {
14009
+ name: "@tacone/prosey",
14010
+ version: "0.2.2",
14011
+ description: "Download YouTube video transcripts from the CLI",
14012
+ module: "src/index.ts",
14013
+ type: "module",
14014
+ bin: {
14015
+ prosey: "bin/prosey"
14016
+ },
14017
+ files: [
14018
+ "bin/",
14019
+ "src/",
14020
+ "package.json",
14021
+ "README.md",
14022
+ "LICENSE"
14023
+ ],
14024
+ scripts: {
14025
+ start: "bun run src/index.ts",
14026
+ build: "bun build src/index.ts --target node --outfile bin/prosey",
14027
+ test: "bun test",
14028
+ typecheck: "tsc --noEmit",
14029
+ prettier: "prettier --write .",
14030
+ prepack: "bun run build",
14031
+ prepare: "husky || true"
14032
+ },
14033
+ "lint-staged": {
14034
+ "*": "prettier --write --ignore-unknown"
14035
+ },
14036
+ author: "tacone <tacone@gmail.com>",
14037
+ license: "MIT",
14038
+ repository: {
14039
+ type: "git",
14040
+ url: "git+https://github.com/tacone/prosey.git"
14041
+ },
14042
+ bugs: {
14043
+ url: "https://github.com/tacone/prosey/issues"
14044
+ },
14045
+ homepage: "https://github.com/tacone/prosey#readme",
14046
+ keywords: [
14047
+ "youtube",
14048
+ "transcript",
14049
+ "subtitles",
14050
+ "captions",
14051
+ "cli",
14052
+ "npx",
14053
+ "bun"
14054
+ ],
14055
+ engines: {
14056
+ node: ">=20"
14057
+ },
14058
+ devDependencies: {
14059
+ "@types/bun": "latest",
14060
+ husky: "^9.1.7",
14061
+ "lint-staged": "^17.0.7",
14062
+ prettier: "^3.8.4"
14063
+ },
14064
+ peerDependencies: {
14065
+ typescript: "^5"
14066
+ },
14067
+ dependencies: {
14068
+ "js-toml": "^1.1.2",
14069
+ "youtube-transcript-plus": "^2.0.0"
14070
+ }
14071
+ };
14072
+
13967
14073
  // src/index.ts
13968
14074
  var NAME2 = "prosey";
13969
- var VERSION2 = "0.1.0";
14075
+ var VERSION2 = package_default.version;
13970
14076
  function help() {
13971
14077
  return `${NAME2} v${VERSION2}
13972
14078
 
@@ -13995,6 +14101,7 @@ Options:
13995
14101
  --no-decode-entities Preserve HTML entities (decoded by default).
13996
14102
  --reset-config Reset config file to defaults and exit.
13997
14103
  --no-cache Skip cache and overwrite cache files.
14104
+ --debug Print debug information to stderr.
13998
14105
  --help Show this help message.
13999
14106
  --version Show version.
14000
14107
 
@@ -14073,12 +14180,10 @@ if (args.includes("--reset-config")) {
14073
14180
  }
14074
14181
  var config = await loadConfig().catch(() => ({}));
14075
14182
  var mode = "transcript";
14076
- if (args[0] === "info") {
14077
- mode = "info";
14078
- args.splice(0, 1);
14079
- } else if (args[0] === "summarize") {
14080
- mode = "summarize";
14081
- args.splice(0, 1);
14183
+ var subcmdIndex = args.findIndex((a2) => a2 === "info" || a2 === "summarize");
14184
+ if (subcmdIndex !== -1) {
14185
+ mode = args[subcmdIndex];
14186
+ args.splice(subcmdIndex, 1);
14082
14187
  }
14083
14188
  var videoId = "";
14084
14189
  var lang;
@@ -14089,6 +14194,7 @@ var outputJson = false;
14089
14194
  var noDecode = false;
14090
14195
  var showDetails = true;
14091
14196
  var noCache = false;
14197
+ var debugMode = false;
14092
14198
  for (let i = 0;i < args.length; i++) {
14093
14199
  const arg = args[i];
14094
14200
  if (!arg)
@@ -14119,6 +14225,8 @@ for (let i = 0;i < args.length; i++) {
14119
14225
  showDetails = false;
14120
14226
  } else if (arg === "--no-cache") {
14121
14227
  noCache = true;
14228
+ } else if (arg === "--debug") {
14229
+ debugMode = true;
14122
14230
  } else if (arg === "--no-decode-entities") {
14123
14231
  noDecode = true;
14124
14232
  } else if (arg.startsWith("-")) {
@@ -14133,6 +14241,19 @@ if (!videoId) {
14133
14241
  console.log(help());
14134
14242
  process.exit(1);
14135
14243
  }
14244
+ var extracted = extractVideoId(videoId);
14245
+ if (!extracted) {
14246
+ console.error("Error: invalid YouTube video URL or ID");
14247
+ process.exit(65);
14248
+ }
14249
+ videoId = extracted;
14250
+ if (debugMode)
14251
+ enableDebug();
14252
+ debug("Config file:", configPath());
14253
+ debug("Video ID:", videoId);
14254
+ debug("Mode:", mode);
14255
+ if (lang)
14256
+ debug("Language:", lang);
14136
14257
  try {
14137
14258
  if (mode === "info") {
14138
14259
  const result = await fetchTranscript(videoId, { videoDetails: true, lang });
@@ -14156,24 +14277,35 @@ try {
14156
14277
  const cachedSegments = await readCache(dir2, "transcript.json");
14157
14278
  const cachedSummary = await readCache(dir2, "summary.md");
14158
14279
  if (cachedSegments && cachedSummary) {
14280
+ debug("Cache hit:", dir2);
14159
14281
  segments2 = JSON.parse(cachedSegments);
14160
14282
  summary = cachedSummary;
14283
+ } else {
14284
+ debug("Cache miss:", dir2);
14161
14285
  }
14286
+ } else {
14287
+ debug("Cache skipped (--no-cache)");
14162
14288
  }
14163
14289
  if (!segments2) {
14290
+ debug("Fetching transcript...");
14164
14291
  segments2 = lang ? await fetchTranscript(videoId, { lang }) : await fetchTranscript(videoId);
14292
+ debug(`Transcript fetched: ${segments2.length} segments`);
14165
14293
  await writeCache(dir2, "transcript.json", JSON.stringify(segments2));
14294
+ debug("Cache written: transcript.json");
14166
14295
  }
14167
14296
  const prompt = config.summarize.prompt ?? "";
14168
14297
  const transcriptText = toText(segments2, !noDecode);
14169
14298
  if (!summary) {
14299
+ debug("Running command:", config.summarize.command);
14170
14300
  summary = await summarize({
14171
14301
  prompt,
14172
14302
  command: config.summarize.command,
14173
14303
  transcript: transcriptText,
14174
14304
  cwd: dir2
14175
14305
  });
14306
+ debug("Command exit: 0");
14176
14307
  await writeCache(dir2, "summary.md", summary);
14308
+ debug("Cache written: summary.md");
14177
14309
  }
14178
14310
  if (outputPath) {
14179
14311
  await writeFile3(outputPath, summary, "utf8");
@@ -14193,10 +14325,17 @@ try {
14193
14325
  let videoDetailsCache = null;
14194
14326
  if (!noCache) {
14195
14327
  const cached = await readCache(dir, "transcript.json");
14196
- if (cached)
14328
+ if (cached) {
14329
+ debug("Cache hit:", dir);
14197
14330
  segments = JSON.parse(cached);
14331
+ } else {
14332
+ debug("Cache miss:", dir);
14333
+ }
14334
+ } else {
14335
+ debug("Cache skipped (--no-cache)");
14198
14336
  }
14199
14337
  if (!segments) {
14338
+ debug("Fetching transcript...");
14200
14339
  if (showDetails && !outputJson) {
14201
14340
  const opts = lang ? { lang, videoDetails: true } : { videoDetails: true };
14202
14341
  const result = await fetchTranscript(videoId, opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacone/prosey",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Download YouTube video transcripts from the CLI",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/src/cache.test.ts CHANGED
@@ -2,7 +2,7 @@ import { describe, expect, test, afterEach } from "bun:test";
2
2
  import { rm, readFile } from "node:fs/promises";
3
3
  import { existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { cacheKey, cacheDir, readCache, writeCache } from "./cache";
5
+ import { cacheKey, cacheDir, readCache, writeCache, extractVideoId } from "./cache";
6
6
 
7
7
  const testDir = "/tmp/prosey/test-cache-spec";
8
8
 
@@ -44,6 +44,34 @@ describe("cacheDir", () => {
44
44
  });
45
45
  });
46
46
 
47
+ describe("extractVideoId", () => {
48
+ test("bare ID passes through", () => {
49
+ expect(extractVideoId("dQw4w9WgXcQ")).toBe("dQw4w9WgXcQ");
50
+ });
51
+
52
+ test("full watch URL", () => {
53
+ expect(extractVideoId("https://www.youtube.com/watch?v=jNAAG3Ma5K8")).toBe("jNAAG3Ma5K8");
54
+ });
55
+
56
+ test("watch URL with extra query params", () => {
57
+ expect(
58
+ extractVideoId("https://www.youtube.com/watch?v=jNAAG3Ma5K8&pp=ygUKc3BhY2V4IGlwbw%3D%3D"),
59
+ ).toBe("jNAAG3Ma5K8");
60
+ });
61
+
62
+ test("short youtu.be URL", () => {
63
+ expect(extractVideoId("https://youtu.be/dQw4w9WgXcQ")).toBe("dQw4w9WgXcQ");
64
+ });
65
+
66
+ test("embed URL", () => {
67
+ expect(extractVideoId("https://www.youtube.com/embed/dQw4w9WgXcQ")).toBe("dQw4w9WgXcQ");
68
+ });
69
+
70
+ test("invalid URL without video ID returns null", () => {
71
+ expect(extractVideoId("https://example.com/search?q=hello")).toBeNull();
72
+ });
73
+ });
74
+
47
75
  describe("readCache / writeCache", () => {
48
76
  test("writes and reads a file", async () => {
49
77
  await writeCache(testDir, "test.txt", "hello world");
package/src/cache.ts CHANGED
@@ -3,6 +3,17 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
3
  import { existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
 
6
+ const RE_YOUTUBE =
7
+ /(?:v=|\/|v\/|embed\/|watch\?.*v=|youtu\.be\/|\/v\/|e\/|watch\?.*vi?=|\/embed\/|\/v\/|vi?\/|watch\?.*vi?=|youtu\.be\/|\/vi?\/|\/e\/)([a-zA-Z0-9_-]{11})/i;
8
+ const RE_BARE_ID = /^[a-zA-Z0-9_-]{11}$/;
9
+
10
+ export function extractVideoId(input: string): string | null {
11
+ if (RE_BARE_ID.test(input)) return input;
12
+ const match = input.match(RE_YOUTUBE);
13
+ if (match) return match[1] || null;
14
+ return null;
15
+ }
16
+
6
17
  export interface CacheOptions {
7
18
  lang?: string;
8
19
  timestamps?: boolean;
package/src/config.ts CHANGED
@@ -19,11 +19,25 @@ const FALLBACK_CONFIG_TOML = `# Default prosey configuration
19
19
  # Prompt sent to the command via stdin.
20
20
  # Customize this to change how transcripts are summarized.
21
21
  prompt = """
22
- Summarize the following transcript.
23
- Focus on the key points and main arguments.
22
+ Write a comprehensive summary of the following transcription.
24
23
  """
25
24
 
26
- # Command to execute with the prompt piped via stdin.
25
+ # Command to execute with the prompt and transcript piped via stdin.
26
+ # The transcript is appended to the prompt automatically.
27
+ #
28
+ # Available options:
29
+ #
30
+ # opencode run — full access (default)
31
+ # opencode run --permissions read — read-only (view files, no edits)
32
+ #
33
+ # claude -p "" --print — full access (--print for clean output)
34
+ # claude --permission-mode plan -p "" --print — read-only (plan/read only)
35
+ #
36
+ # copilot -sp "" — full access (-s = silent, -p = prompt)
37
+ # copilot -sp "" --deny-all-tools — read-only (no shell/write access)
38
+ #
39
+ # codex --sandbox default -p "" — full access
40
+ # codex --sandbox read-only -p "" — read-only
27
41
  command = "opencode run"
28
42
  `;
29
43
 
package/src/debug.ts ADDED
@@ -0,0 +1,13 @@
1
+ const GRAY = "\x1b[90m";
2
+ const RESET = "\x1b[0m";
3
+
4
+ let enabled = false;
5
+
6
+ export function enableDebug(): void {
7
+ enabled = true;
8
+ }
9
+
10
+ export function debug(...args: unknown[]): void {
11
+ if (!enabled) return;
12
+ console.error(GRAY, ...args, RESET);
13
+ }
@@ -2,11 +2,15 @@
2
2
  # Created automatically on first run. Edit as needed.
3
3
 
4
4
  [summarize]
5
+
5
6
  # Prompt sent to the command via stdin.
6
7
  # Customize this to change how transcripts are summarized.
8
+
7
9
  prompt = """
8
10
  Write a comprehensive summary of the following transcription.
9
11
  """
10
12
 
11
- # Command to execute with the prompt piped via stdin.
12
- command = "opencode run"
13
+ # Command to execute with the prompt and transcript piped via stdin.
14
+ # The transcript is appended to the prompt automatically.
15
+
16
+ command = "OPENCODE_PERMISSION='{\"read\":\"allow\",\"write\":\"deny\",\"edit\":\"deny\",\"bash\":\"deny\",\"glob\":\"deny\",\"grep\":\"deny\",\"webfetch\":\"deny\",\"task\":\"deny\",\"todowrite\":\"deny\",\"websearch\":\"deny\",\"lsp\":\"deny\"}' opencode run --pure"
package/src/index.ts CHANGED
@@ -4,13 +4,15 @@ import { writeFile } from "node:fs/promises";
4
4
  import { fetchTranscript, listLanguages } from "youtube-transcript-plus";
5
5
  import type { CaptionTrackInfo, VideoDetails, TranscriptSegment } from "youtube-transcript-plus";
6
6
  import { formatWithTimestamps, toText, toJSON, formatDuration, decodeEntities } from "./format";
7
- import { loadConfig, resetConfig } from "./config";
7
+ import { loadConfig, resetConfig, configPath } from "./config";
8
8
  import type { ProseyConfig } from "./config";
9
9
  import { summarize } from "./summarize";
10
- import { cacheDir, readCache, writeCache } from "./cache";
10
+ import { cacheDir, readCache, writeCache, extractVideoId } from "./cache";
11
+ import { enableDebug, debug } from "./debug";
12
+ import pkg from "../package.json";
11
13
 
12
14
  const NAME = "prosey";
13
- const VERSION = "0.1.0";
15
+ const VERSION = pkg.version;
14
16
 
15
17
  function help(): string {
16
18
  return `${NAME} v${VERSION}
@@ -40,6 +42,7 @@ Options:
40
42
  --no-decode-entities Preserve HTML entities (decoded by default).
41
43
  --reset-config Reset config file to defaults and exit.
42
44
  --no-cache Skip cache and overwrite cache files.
45
+ --debug Print debug information to stderr.
43
46
  --help Show this help message.
44
47
  --version Show version.
45
48
 
@@ -138,12 +141,10 @@ if (args.includes("--reset-config")) {
138
141
  const config: ProseyConfig = await loadConfig().catch(() => ({}) as ProseyConfig);
139
142
 
140
143
  let mode = "transcript";
141
- if (args[0] === "info") {
142
- mode = "info";
143
- args.splice(0, 1);
144
- } else if (args[0] === "summarize") {
145
- mode = "summarize";
146
- args.splice(0, 1);
144
+ const subcmdIndex = args.findIndex((a) => a === "info" || a === "summarize");
145
+ if (subcmdIndex !== -1) {
146
+ mode = args[subcmdIndex]!;
147
+ args.splice(subcmdIndex, 1);
147
148
  }
148
149
 
149
150
  let videoId = "";
@@ -155,6 +156,7 @@ let outputJson = false;
155
156
  let noDecode = false;
156
157
  let showDetails = true;
157
158
  let noCache = false;
159
+ let debugMode = false;
158
160
 
159
161
  for (let i = 0; i < args.length; i++) {
160
162
  const arg = args[i];
@@ -185,6 +187,8 @@ for (let i = 0; i < args.length; i++) {
185
187
  showDetails = false;
186
188
  } else if (arg === "--no-cache") {
187
189
  noCache = true;
190
+ } else if (arg === "--debug") {
191
+ debugMode = true;
188
192
  } else if (arg === "--no-decode-entities") {
189
193
  noDecode = true;
190
194
  } else if (arg.startsWith("-")) {
@@ -201,6 +205,21 @@ if (!videoId) {
201
205
  process.exit(1);
202
206
  }
203
207
 
208
+ const extracted = extractVideoId(videoId);
209
+
210
+ if (!extracted) {
211
+ console.error("Error: invalid YouTube video URL or ID");
212
+ process.exit(65);
213
+ }
214
+
215
+ videoId = extracted;
216
+
217
+ if (debugMode) enableDebug();
218
+ debug("Config file:", configPath());
219
+ debug("Video ID:", videoId);
220
+ debug("Mode:", mode);
221
+ if (lang) debug("Language:", lang);
222
+
204
223
  try {
205
224
  if (mode === "info") {
206
225
  const result = await fetchTranscript(videoId, { videoDetails: true, lang } as any);
@@ -227,27 +246,38 @@ try {
227
246
  const cachedSegments = await readCache(dir, "transcript.json");
228
247
  const cachedSummary = await readCache(dir, "summary.md");
229
248
  if (cachedSegments && cachedSummary) {
249
+ debug("Cache hit:", dir);
230
250
  segments = JSON.parse(cachedSegments);
231
251
  summary = cachedSummary;
252
+ } else {
253
+ debug("Cache miss:", dir);
232
254
  }
255
+ } else {
256
+ debug("Cache skipped (--no-cache)");
233
257
  }
234
258
 
235
259
  if (!segments) {
260
+ debug("Fetching transcript...");
236
261
  segments = lang ? await fetchTranscript(videoId, { lang }) : await fetchTranscript(videoId);
262
+ debug(`Transcript fetched: ${segments.length} segments`);
237
263
  await writeCache(dir, "transcript.json", JSON.stringify(segments));
264
+ debug("Cache written: transcript.json");
238
265
  }
239
266
 
240
267
  const prompt = config.summarize.prompt ?? "";
241
268
  const transcriptText = toText(segments, !noDecode);
242
269
 
243
270
  if (!summary) {
271
+ debug("Running command:", config.summarize.command);
244
272
  summary = await summarize({
245
273
  prompt,
246
274
  command: config.summarize.command,
247
275
  transcript: transcriptText,
248
276
  cwd: dir,
249
277
  });
278
+ debug("Command exit: 0");
250
279
  await writeCache(dir, "summary.md", summary);
280
+ debug("Cache written: summary.md");
251
281
  }
252
282
 
253
283
  if (outputPath) {
@@ -270,10 +300,18 @@ try {
270
300
 
271
301
  if (!noCache) {
272
302
  const cached = await readCache(dir, "transcript.json");
273
- if (cached) segments = JSON.parse(cached);
303
+ if (cached) {
304
+ debug("Cache hit:", dir);
305
+ segments = JSON.parse(cached);
306
+ } else {
307
+ debug("Cache miss:", dir);
308
+ }
309
+ } else {
310
+ debug("Cache skipped (--no-cache)");
274
311
  }
275
312
 
276
313
  if (!segments) {
314
+ debug("Fetching transcript...");
277
315
  if (showDetails && !outputJson) {
278
316
  const opts = lang ? { lang, videoDetails: true as const } : { videoDetails: true as const };
279
317
  const result = (await fetchTranscript(videoId, opts)) as {
@@ -2,22 +2,24 @@ import { describe, expect, test } from "bun:test";
2
2
  import { summarize } from "./summarize";
3
3
 
4
4
  describe("summarize", () => {
5
- test("strips input text from output", async () => {
6
- const result = await summarize({
7
- prompt: "Summarize this:",
8
- command: "cat",
9
- transcript: "Hello world. This is the transcript.",
10
- });
11
- expect(result).toBe("");
5
+ test("rejects when command only echoes input", async () => {
6
+ await expect(
7
+ summarize({
8
+ prompt: "Summarize this:",
9
+ command: "cat",
10
+ transcript: "Hello world. This is the transcript.",
11
+ }),
12
+ ).rejects.toThrow("Summarization command returned no meaningful output");
12
13
  });
13
14
 
14
- test("strips input text from output with empty prompt", async () => {
15
- const result = await summarize({
16
- prompt: "",
17
- command: "cat",
18
- transcript: "Just the transcript.",
19
- });
20
- expect(result).toBe("");
15
+ test("rejects when command only echoes input with empty prompt", async () => {
16
+ await expect(
17
+ summarize({
18
+ prompt: "",
19
+ command: "cat",
20
+ transcript: "Just the transcript.",
21
+ }),
22
+ ).rejects.toThrow("Summarization command returned no meaningful output");
21
23
  });
22
24
 
23
25
  test("preserves response text beyond the input", async () => {
package/src/summarize.ts CHANGED
@@ -38,5 +38,14 @@ export async function summarize(options: SummarizeOptions): Promise<string> {
38
38
  const { prompt, command, transcript, cwd } = options;
39
39
  const fullPrompt = `${prompt}\n\n${transcript}`;
40
40
  const output = await executeCommand(command, fullPrompt, cwd);
41
- return output.replace(fullPrompt, "").replace(/\n+$/, "");
41
+
42
+ const cleaned = output.startsWith(fullPrompt)
43
+ ? output.slice(fullPrompt.length).replace(/\n+$/, "")
44
+ : output.replace(/\n+$/, "");
45
+
46
+ if (!cleaned || cleaned === transcript) {
47
+ throw new Error("Summarization command returned no meaningful output");
48
+ }
49
+
50
+ return cleaned;
42
51
  }