@theplato/tiro-cli 0.1.0 → 0.2.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.
package/dist/bin/tiro.js CHANGED
@@ -722,15 +722,47 @@ var NoteSchema = z2.object({
722
722
  recordingStartAt: z2.string().nullable().optional(),
723
723
  recordingEndAt: z2.string().nullable().optional()
724
724
  }).passthrough();
725
+ var TextObjectSchema = z2.object({
726
+ type: z2.string(),
727
+ content: z2.string()
728
+ });
729
+ var SpeakerInfoSchema = z2.object({
730
+ label: z2.string(),
731
+ personName: z2.string().nullable().optional()
732
+ });
733
+ var DiarizedSegmentSchema = z2.object({
734
+ content: z2.string(),
735
+ speaker: SpeakerInfoSchema
736
+ });
725
737
  var ParagraphSchema = z2.object({
726
- text: z2.string(),
727
- startMs: z2.number().nullable().optional(),
728
- endMs: z2.number().nullable().optional(),
729
- speaker: z2.object({
730
- name: z2.string().nullable().optional(),
731
- email: z2.string().nullable().optional()
732
- }).nullable().optional()
738
+ uuid: z2.string(),
739
+ transcribeLocale: z2.string().nullable().optional(),
740
+ transcript: TextObjectSchema.nullable().optional(),
741
+ diarizedSegments: z2.array(DiarizedSegmentSchema).nullable().optional(),
742
+ timeFrom: z2.string().nullable().optional(),
743
+ timeTo: z2.string().nullable().optional(),
744
+ locked: z2.boolean().optional()
733
745
  }).passthrough();
746
+ var McpSegmentSchema = z2.object({
747
+ content: z2.string(),
748
+ speaker: z2.object({
749
+ label: z2.string(),
750
+ name: z2.string().nullable()
751
+ }).nullable()
752
+ });
753
+ var McpParagraphSchema = z2.object({
754
+ timeFrom: z2.string().nullable(),
755
+ timeTo: z2.string().nullable(),
756
+ segments: z2.array(McpSegmentSchema)
757
+ });
758
+ var McpTranscriptSchema = z2.object({
759
+ noteGuid: z2.string(),
760
+ title: z2.string(),
761
+ participants: z2.array(z2.string()),
762
+ createdAt: z2.string(),
763
+ recordingDurationSeconds: z2.number(),
764
+ paragraphs: z2.array(McpParagraphSchema)
765
+ });
734
766
  var PageCursorResponseSchema = (item) => z2.object({
735
767
  content: z2.array(item),
736
768
  nextCursor: z2.string().nullable()
@@ -903,18 +935,62 @@ function createApiClient(opts = {}) {
903
935
  return new TiroApiClient(hostname, t.accessToken);
904
936
  }
905
937
 
938
+ // src/lib/util/parseDate.ts
939
+ var RELATIVE_RE = /^(\d+)([smhdw])$/i;
940
+ var UNIT_MS = {
941
+ s: 1e3,
942
+ m: 6e4,
943
+ h: 36e5,
944
+ d: 864e5,
945
+ w: 6048e5
946
+ };
947
+ function parseDate(input) {
948
+ const trimmed = input.trim();
949
+ const rel = trimmed.match(RELATIVE_RE);
950
+ if (rel) {
951
+ const num = parseInt(rel[1] ?? "", 10);
952
+ const unit = (rel[2] ?? "").toLowerCase();
953
+ const factor = UNIT_MS[unit];
954
+ if (!factor || !Number.isFinite(num)) {
955
+ throw invalidDate(input);
956
+ }
957
+ return new Date(Date.now() - num * factor).toISOString();
958
+ }
959
+ const d = new Date(trimmed);
960
+ if (Number.isNaN(d.getTime())) throw invalidDate(input);
961
+ return d.toISOString();
962
+ }
963
+ function invalidDate(input) {
964
+ return new TiroError(
965
+ {
966
+ code: "invalid_date",
967
+ message: `Invalid date: "${input}". Use ISO-8601 (e.g. 2026-04-01T10:00:00Z) or relative (e.g. 7d, 24h, 30m).`,
968
+ errorType: "bad_request"
969
+ },
970
+ ExitCode.Usage
971
+ );
972
+ }
973
+
906
974
  // src/commands/notes/list.ts
907
975
  var ListResponseSchema = PageCursorResponseSchema(NoteSchema);
976
+ var DEFAULT_PAGE_SIZE = 100;
977
+ var MAX_PAGE_SIZE = 1e3;
908
978
  function registerNotesList(parent) {
909
- parent.command("list").description("List recent notes").option("--folder <id>", "Filter by folder ID").option("--limit <n>", "Max results per page (default 50, max 500)").option("--cursor <token>", "Cursor for the next page").action(async (opts, cmd) => {
979
+ parent.command("list").description("List notes (lightweight metadata)").option("--keyword <text>", "Reorder by OpenSearch relevance for this keyword").option("--folder <id>", "Restrict to a folder (recursive)").option("--since <date>", "ISO-8601 or relative (7d, 24h, 30m); maps to createdAtFrom").option("--until <date>", "ISO-8601 or relative; maps to createdAtTo").option(
980
+ "--limit <n>",
981
+ `Page size (default ${DEFAULT_PAGE_SIZE}, max ${MAX_PAGE_SIZE})`
982
+ ).option("--cursor <token>", "Cursor for the next page").action(async (opts, cmd) => {
910
983
  const globalOpts = cmd.optsWithGlobals();
911
- const limit = clampLimit(opts.limit);
912
984
  const client = createApiClient({
913
985
  ...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
914
986
  });
915
987
  const params = {};
988
+ if (opts.keyword) params["keyword"] = opts.keyword;
916
989
  if (opts.folder) params["folderId"] = opts.folder;
917
- if (limit !== void 0) params["size"] = limit;
990
+ if (opts.since) params["createdAtFrom"] = parseDate(opts.since);
991
+ if (opts.until) params["createdAtTo"] = parseDate(opts.until);
992
+ const size = clampLimit(opts.limit);
993
+ if (size !== void 0) params["size"] = size;
918
994
  if (opts.cursor) params["cursor"] = opts.cursor;
919
995
  const res = await client.getJson("/v1/external/notes", ListResponseSchema, params);
920
996
  const mode = resolveOutputMode(globalOpts);
@@ -929,8 +1005,8 @@ function registerNotesList(parent) {
929
1005
  function clampLimit(raw) {
930
1006
  if (!raw) return void 0;
931
1007
  const n = parseInt(raw, 10);
932
- if (!Number.isFinite(n) || n <= 0) return 50;
933
- return Math.min(n, 500);
1008
+ if (!Number.isFinite(n) || n <= 0) return DEFAULT_PAGE_SIZE;
1009
+ return Math.min(n, MAX_PAGE_SIZE);
934
1010
  }
935
1011
  function printPretty(notes, nextCursor, opts) {
936
1012
  if (notes.length === 0) {
@@ -938,11 +1014,13 @@ function printPretty(notes, nextCursor, opts) {
938
1014
  `);
939
1015
  return;
940
1016
  }
1017
+ const titleWidth = computeTitleWidth();
941
1018
  for (const n of notes) {
942
1019
  const date = n.createdAt.slice(0, 10);
943
1020
  const dur = formatDuration(n.recordingDurationSeconds);
1021
+ const title = truncate(n.title, titleWidth);
944
1022
  process.stdout.write(
945
- `${color(date, "gray", opts)} ${color(n.guid, "dim", opts)} ${color(dur, "cyan", opts)} ${n.title}
1023
+ `${color(date, "gray", opts)} ${color(n.guid, "dim", opts)} ${color(dur, "cyan", opts)} ${title}
946
1024
  `
947
1025
  );
948
1026
  }
@@ -954,6 +1032,15 @@ function printPretty(notes, nextCursor, opts) {
954
1032
  );
955
1033
  }
956
1034
  }
1035
+ function computeTitleWidth() {
1036
+ const cols = process.stdout.columns;
1037
+ if (!cols || cols < 60) return 40;
1038
+ return Math.max(20, cols - 60);
1039
+ }
1040
+ function truncate(s, max) {
1041
+ if (s.length <= max) return s;
1042
+ return s.slice(0, Math.max(0, max - 1)) + "\u2026";
1043
+ }
957
1044
  function formatDuration(sec) {
958
1045
  if (!sec || sec <= 0) return "\u2014";
959
1046
  const m = Math.floor(sec / 60);
@@ -964,60 +1051,38 @@ function formatDuration(sec) {
964
1051
 
965
1052
  // src/commands/notes/search.ts
966
1053
  import "commander";
967
-
968
- // src/lib/util/parseDate.ts
969
- var RELATIVE_RE = /^(\d+)([smhdw])$/i;
970
- var UNIT_MS = {
971
- s: 1e3,
972
- m: 6e4,
973
- h: 36e5,
974
- d: 864e5,
975
- w: 6048e5
976
- };
977
- function parseDate(input) {
978
- const trimmed = input.trim();
979
- const rel = trimmed.match(RELATIVE_RE);
980
- if (rel) {
981
- const num = parseInt(rel[1] ?? "", 10);
982
- const unit = (rel[2] ?? "").toLowerCase();
983
- const factor = UNIT_MS[unit];
984
- if (!factor || !Number.isFinite(num)) {
985
- throw invalidDate(input);
986
- }
987
- return new Date(Date.now() - num * factor).toISOString();
988
- }
989
- const d = new Date(trimmed);
990
- if (Number.isNaN(d.getTime())) throw invalidDate(input);
991
- return d.toISOString();
992
- }
993
- function invalidDate(input) {
994
- return new TiroError(
995
- {
996
- code: "invalid_date",
997
- message: `Invalid date: "${input}". Use ISO-8601 (e.g. 2026-04-01T10:00:00Z) or relative (e.g. 7d, 24h, 30m).`,
998
- errorType: "bad_request"
999
- },
1000
- ExitCode.Usage
1001
- );
1002
- }
1003
-
1004
- // src/commands/notes/search.ts
1005
- var SearchResponseSchema = PageCursorResponseSchema(NoteSchema);
1054
+ var SearchResponseSchema = PageCursorResponseSchema(NoteSchema).passthrough();
1055
+ var DEFAULT_PAGE_SIZE2 = 100;
1056
+ var MAX_PAGE_SIZE2 = 1e3;
1006
1057
  function registerNotesSearch(parent) {
1007
- parent.command("search [query]").description("Search notes by speaker / date / folder / keyword").option("--speaker <name>", "Filter by speaker name").option("--since <date>", "Notes created after this date (ISO-8601 or 7d, 24h, 30m)").option("--until <date>", "Notes created before this date").option("--folder <id>", "Filter by folder ID").option("--limit <n>", "Max results per page (default 50, max 500)").option("--cursor <token>", "Cursor for the next page").action(async (query, opts, cmd) => {
1058
+ parent.command("search [keyword]").description("Deep keyword search \u2014 returns notes hydrated with their primary documents").option("--keyword <text>", "Alternative to positional keyword").option("--folder <id>", "Restrict hits to a folder (recursive)").option("--since <date>", "ISO-8601 or relative (7d, 24h, 30m); maps to filter.createdAtFrom").option("--until <date>", "ISO-8601 or relative; maps to filter.createdAtTo").option("--limit <n>", `Page size (default ${DEFAULT_PAGE_SIZE2}, max ${MAX_PAGE_SIZE2})`).option("--cursor <token>", "Cursor for the next page (reserved \u2014 currently always null)").action(async (positional, opts, cmd) => {
1008
1059
  const globalOpts = cmd.optsWithGlobals();
1060
+ const keyword = (positional ?? opts.keyword ?? "").trim();
1061
+ if (!keyword) {
1062
+ throw new TiroError(
1063
+ {
1064
+ code: "missing_keyword",
1065
+ message: "search requires a keyword (positional or --keyword).",
1066
+ errorType: "bad_request",
1067
+ suggestion: 'tiro notes search "OKR"'
1068
+ },
1069
+ ExitCode.Usage
1070
+ );
1071
+ }
1072
+ const filter = {};
1073
+ if (opts.folder) filter["folderId"] = opts.folder;
1074
+ if (opts.since) filter["createdAtFrom"] = parseDate(opts.since);
1075
+ if (opts.until) filter["createdAtTo"] = parseDate(opts.until);
1076
+ const pagination = {};
1077
+ const size = clampLimit2(opts.limit);
1078
+ if (size !== void 0) pagination["size"] = size;
1079
+ if (opts.cursor) pagination["cursor"] = opts.cursor;
1080
+ const body = { keyword };
1081
+ if (Object.keys(filter).length > 0) body["filter"] = filter;
1082
+ if (Object.keys(pagination).length > 0) body["pagination"] = pagination;
1009
1083
  const client = createApiClient({
1010
1084
  ...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
1011
1085
  });
1012
- const body = {};
1013
- if (query) body["query"] = query;
1014
- if (opts.speaker) body["speaker"] = opts.speaker;
1015
- if (opts.since) body["since"] = parseDate(opts.since);
1016
- if (opts.until) body["until"] = parseDate(opts.until);
1017
- if (opts.folder) body["folderId"] = opts.folder;
1018
- const limit = clampLimit2(opts.limit);
1019
- if (limit !== void 0) body["size"] = limit;
1020
- if (opts.cursor) body["cursor"] = opts.cursor;
1021
1086
  const res = await client.postJson(
1022
1087
  "/v1/external/notes/search",
1023
1088
  SearchResponseSchema,
@@ -1035,8 +1100,8 @@ function registerNotesSearch(parent) {
1035
1100
  function clampLimit2(raw) {
1036
1101
  if (!raw) return void 0;
1037
1102
  const n = parseInt(raw, 10);
1038
- if (!Number.isFinite(n) || n <= 0) return 50;
1039
- return Math.min(n, 500);
1103
+ if (!Number.isFinite(n) || n <= 0) return DEFAULT_PAGE_SIZE2;
1104
+ return Math.min(n, MAX_PAGE_SIZE2);
1040
1105
  }
1041
1106
  function printPretty2(notes, nextCursor, opts) {
1042
1107
  if (notes.length === 0) {
@@ -1098,6 +1163,98 @@ async function fileExists(p) {
1098
1163
  }
1099
1164
  }
1100
1165
 
1166
+ // src/lib/output/transcript.ts
1167
+ function buildMcpTranscript(note, paragraphs) {
1168
+ return {
1169
+ noteGuid: note.guid,
1170
+ title: note.title,
1171
+ participants: note.participants?.map((p) => p.name || p.email || "").filter((s) => typeof s === "string" && s.length > 0) ?? [],
1172
+ createdAt: note.createdAt,
1173
+ recordingDurationSeconds: note.recordingDurationSeconds,
1174
+ paragraphs: paragraphsToMcp(paragraphs)
1175
+ };
1176
+ }
1177
+ function paragraphsToMcp(paragraphs) {
1178
+ return paragraphs.map((p) => ({
1179
+ timeFrom: p.timeFrom ?? null,
1180
+ timeTo: p.timeTo ?? null,
1181
+ segments: paragraphToSegments(p)
1182
+ })).filter((p) => p.segments.length > 0);
1183
+ }
1184
+ function paragraphToSegments(p) {
1185
+ const ds = p.diarizedSegments;
1186
+ if (ds && ds.length > 0) {
1187
+ return ds.map((s) => ({
1188
+ content: stripHtml(s.content),
1189
+ speaker: {
1190
+ label: s.speaker.label,
1191
+ name: s.speaker.personName ? stripHtml(s.speaker.personName) : null
1192
+ }
1193
+ })).filter((s) => s.content.length > 0);
1194
+ }
1195
+ const plain = stripHtml(p.transcript?.content ?? "");
1196
+ return plain ? [{ content: plain, speaker: null }] : [];
1197
+ }
1198
+ function renderTranscriptJson(t) {
1199
+ return `${JSON.stringify(t, null, 2)}
1200
+ `;
1201
+ }
1202
+ function renderTranscriptMarkdown(t) {
1203
+ const anchor = anchorTime(t);
1204
+ const lines = [];
1205
+ lines.push(`# ${t.title}`, "");
1206
+ if (t.participants.length > 0) {
1207
+ lines.push(`**Participants**: ${t.participants.join(", ")}`, "");
1208
+ }
1209
+ lines.push("## Transcript", "");
1210
+ for (const p of t.paragraphs) {
1211
+ const ts = elapsed(p.timeFrom, anchor);
1212
+ for (const s of p.segments) {
1213
+ const who = s.speaker?.name ?? s.speaker?.label ?? "Unknown";
1214
+ const tag = ts ? `${who}, ${ts}` : who;
1215
+ lines.push(`**[${tag}]** ${s.content}`);
1216
+ }
1217
+ lines.push("");
1218
+ }
1219
+ return `${lines.join("\n").trimEnd()}
1220
+ `;
1221
+ }
1222
+ function renderTranscriptText(t) {
1223
+ const lines = [];
1224
+ for (const p of t.paragraphs) {
1225
+ for (const s of p.segments) {
1226
+ const who = s.speaker?.name ?? s.speaker?.label ?? "Unknown";
1227
+ lines.push(`[${who}] ${s.content}`);
1228
+ }
1229
+ }
1230
+ return `${lines.join("\n")}
1231
+ `;
1232
+ }
1233
+ function anchorTime(t) {
1234
+ for (const p of t.paragraphs) {
1235
+ if (p.timeFrom) return p.timeFrom;
1236
+ }
1237
+ return null;
1238
+ }
1239
+ function elapsed(currentIso, anchorIso) {
1240
+ if (!currentIso || !anchorIso) return "";
1241
+ const cur = Date.parse(currentIso);
1242
+ const anc = Date.parse(anchorIso);
1243
+ if (!Number.isFinite(cur) || !Number.isFinite(anc)) return "";
1244
+ const seconds = Math.max(0, Math.floor((cur - anc) / 1e3));
1245
+ const h = Math.floor(seconds / 3600);
1246
+ const m = Math.floor(seconds % 3600 / 60);
1247
+ const s = seconds % 60;
1248
+ if (h > 0) return `${pad(h)}:${pad(m)}:${pad(s)}`;
1249
+ return `${pad(m)}:${pad(s)}`;
1250
+ }
1251
+ function pad(n) {
1252
+ return n.toString().padStart(2, "0");
1253
+ }
1254
+ function stripHtml(s) {
1255
+ return s.replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
1256
+ }
1257
+
1101
1258
  // src/lib/output/format.ts
1102
1259
  function formatNote(note, format, opts = {}) {
1103
1260
  switch (format) {
@@ -1134,20 +1291,19 @@ function formatMarkdown(note, opts) {
1134
1291
  parts.push("");
1135
1292
  }
1136
1293
  if (opts.includeTranscript && opts.paragraphs && opts.paragraphs.length > 0) {
1137
- parts.push("## Transcript", "");
1138
- for (const p of opts.paragraphs) {
1139
- const speaker = p.speaker?.name ?? "Unknown";
1140
- const ts = formatTimestamp(p.startMs ?? null);
1141
- parts.push(`**[${speaker}${ts ? `, ${ts}` : ""}]** ${p.text}`);
1142
- parts.push("");
1143
- }
1294
+ const mcp = buildMcpTranscript(note, opts.paragraphs);
1295
+ const transcriptBody = renderTranscriptMarkdown(mcp);
1296
+ const startsWithHeader = transcriptBody.startsWith(`# ${note.title}`);
1297
+ const trimmed = startsWithHeader ? transcriptBody.slice(transcriptBody.indexOf("\n") + 1) : transcriptBody;
1298
+ parts.push(trimmed.replace(/^\s*\n+/, ""));
1144
1299
  }
1145
- return parts.join("\n");
1300
+ return `${parts.join("\n").trimEnd()}
1301
+ `;
1146
1302
  }
1147
1303
  function formatJson(note, opts) {
1148
1304
  const out = { ...note };
1149
1305
  if (opts.includeTranscript && opts.paragraphs) {
1150
- out["transcript"] = { paragraphs: opts.paragraphs };
1306
+ out["transcript"] = { paragraphs: paragraphsToMcp(opts.paragraphs) };
1151
1307
  }
1152
1308
  return `${JSON.stringify(out, null, 2)}
1153
1309
  `;
@@ -1158,21 +1314,8 @@ function formatText(note, opts) {
1158
1314
  ${note.webUrl}
1159
1315
  `;
1160
1316
  }
1161
- return opts.paragraphs.map((p) => `[${p.speaker?.name ?? "Unknown"}] ${p.text}`).join("\n");
1162
- }
1163
- function formatTimestamp(ms) {
1164
- if (ms === null || ms === void 0 || !Number.isFinite(ms)) return "";
1165
- const totalSec = Math.floor(ms / 1e3);
1166
- const h = Math.floor(totalSec / 3600);
1167
- const m = Math.floor(totalSec % 3600 / 60);
1168
- const s = totalSec % 60;
1169
- if (h > 0) {
1170
- return `${pad(h)}:${pad(m)}:${pad(s)}`;
1171
- }
1172
- return `${pad(m)}:${pad(s)}`;
1173
- }
1174
- function pad(n) {
1175
- return n.toString().padStart(2, "0");
1317
+ const mcp = buildMcpTranscript(note, opts.paragraphs);
1318
+ return renderTranscriptText(mcp);
1176
1319
  }
1177
1320
  function escapeYaml(s) {
1178
1321
  if (/[:#\n"']/.test(s)) {
@@ -1182,20 +1325,22 @@ function escapeYaml(s) {
1182
1325
  }
1183
1326
 
1184
1327
  // src/commands/notes/get.ts
1328
+ var ALLOWED_INCLUDES = /* @__PURE__ */ new Set(["transcript"]);
1185
1329
  var ParagraphsListSchema = SimpleListResponseSchema(ParagraphSchema);
1186
1330
  var ParagraphsCursorSchema = PageCursorResponseSchema(ParagraphSchema);
1187
1331
  function registerNotesGet(parent) {
1188
1332
  parent.command("get <guid>").description("Get a single note (stdout or file)").option("--output <path>", "Write to file (stdout becomes metadata only)").option("--format <md|json|txt>", "Output format (default: md for TTY, json for pipe)").option(
1189
1333
  "--include <items>",
1190
- "Comma-separated extras: transcript",
1334
+ "Comma-separated extras (v0.2.0 supports: transcript)",
1191
1335
  ""
1192
1336
  ).option("--force", "Overwrite existing file").action(async (guid, opts, cmd) => {
1193
1337
  const globalOpts = cmd.optsWithGlobals();
1338
+ const includes = parseIncludes(opts.include);
1339
+ validateIncludes(includes);
1340
+ const format = pickFormat(opts.format, opts.output);
1194
1341
  const client = createApiClient({
1195
1342
  ...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
1196
1343
  });
1197
- const includes = parseIncludes(opts.include);
1198
- const format = pickFormat(opts.format, opts.output);
1199
1344
  const note = await client.getJson(`/v1/external/notes/${guid}`, NoteSchema);
1200
1345
  let paragraphs;
1201
1346
  if (includes.has("transcript") || format === "txt") {
@@ -1231,7 +1376,7 @@ function registerNotesGet(parent) {
1231
1376
  ok: true,
1232
1377
  data: {
1233
1378
  ...note,
1234
- ...paragraphs && { transcript: { paragraphs } }
1379
+ ...paragraphs && { transcript: { paragraphs: paragraphsToMcp(paragraphs) } }
1235
1380
  }
1236
1381
  },
1237
1382
  globalOpts
@@ -1239,8 +1384,9 @@ function registerNotesGet(parent) {
1239
1384
  } else if (format === "json") {
1240
1385
  process.stdout.write(content);
1241
1386
  } else {
1242
- if (process.stdout.isTTY) {
1387
+ if (process.stdout.isTTY && format === "txt") {
1243
1388
  process.stdout.write(`${color(`# ${note.title}`, "bold", globalOpts)}
1389
+
1244
1390
  `);
1245
1391
  }
1246
1392
  process.stdout.write(content);
@@ -1248,13 +1394,13 @@ function registerNotesGet(parent) {
1248
1394
  });
1249
1395
  }
1250
1396
  async function fetchAllParagraphs(client, guid) {
1251
- const tryCursor = await client.getJson(
1397
+ const first = await client.getJson(
1252
1398
  `/v1/external/notes/${guid}/paragraphs`,
1253
1399
  ParagraphsCursorSchema.or(ParagraphsListSchema)
1254
1400
  );
1255
- const all = [...tryCursor.content];
1256
- if ("nextCursor" in tryCursor) {
1257
- let cursor = tryCursor.nextCursor;
1401
+ const all = [...first.content];
1402
+ if ("nextCursor" in first) {
1403
+ let cursor = first.nextCursor;
1258
1404
  while (cursor) {
1259
1405
  const next = await client.getJson(
1260
1406
  `/v1/external/notes/${guid}/paragraphs`,
@@ -1273,6 +1419,21 @@ function parseIncludes(raw) {
1273
1419
  raw.split(",").map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0)
1274
1420
  );
1275
1421
  }
1422
+ function validateIncludes(includes) {
1423
+ for (const inc of includes) {
1424
+ if (!ALLOWED_INCLUDES.has(inc)) {
1425
+ throw new TiroError(
1426
+ {
1427
+ code: "invalid_include",
1428
+ message: `Invalid --include "${inc}". v0.2.0 supports: transcript.`,
1429
+ errorType: "bad_request",
1430
+ suggestion: "Use --include transcript"
1431
+ },
1432
+ ExitCode.Usage
1433
+ );
1434
+ }
1435
+ }
1436
+ }
1276
1437
  function pickFormat(format, output) {
1277
1438
  const allowed = ["md", "json", "txt"];
1278
1439
  if (format) {
@@ -1302,18 +1463,19 @@ import "commander";
1302
1463
  var ParagraphsListSchema2 = SimpleListResponseSchema(ParagraphSchema);
1303
1464
  var ParagraphsCursorSchema2 = PageCursorResponseSchema(ParagraphSchema);
1304
1465
  function registerNotesTranscript(parent) {
1305
- parent.command("transcript <guid>").description("Get raw transcript paragraphs of a note").option("--output <path>", "Write to file (stdout = metadata only)").option("--format <md|json|txt>", "Output format (default: txt for transcript)").option("--force", "Overwrite existing file").action(async (guid, opts, cmd) => {
1466
+ parent.command("transcript <guid>").description("Get the full transcript of a note (matches MCP get_note_transcript JSON shape)").option("--output <path>", "Write to file (stdout = metadata only)").option(
1467
+ "--format <md|json|txt>",
1468
+ "Output format (default: md for TTY, txt for pipe; json mirrors MCP shape)"
1469
+ ).option("--force", "Overwrite existing file").action(async (guid, opts, cmd) => {
1306
1470
  const globalOpts = cmd.optsWithGlobals();
1471
+ const format = pickFormat2(opts.format, opts.output, globalOpts);
1307
1472
  const client = createApiClient({
1308
1473
  ...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
1309
1474
  });
1310
- const format = pickFormat2(opts.format, opts.output);
1311
1475
  const note = await client.getJson(`/v1/external/notes/${guid}`, NoteSchema);
1312
1476
  const paragraphs = await fetchAllParagraphs2(client, guid);
1313
- const content = formatNote(note, format, {
1314
- includeTranscript: true,
1315
- paragraphs
1316
- });
1477
+ const mcp = buildMcpTranscript(note, paragraphs);
1478
+ const content = format === "json" ? renderTranscriptJson(mcp) : format === "md" ? renderTranscriptMarkdown(mcp) : renderTranscriptText(mcp);
1317
1479
  if (opts.output) {
1318
1480
  const result = await writeFileAtomic(opts.output, content, {
1319
1481
  ...opts.force === true && { force: true }
@@ -1326,7 +1488,8 @@ function registerNotesTranscript(parent) {
1326
1488
  size: result.size,
1327
1489
  format,
1328
1490
  guid: note.guid,
1329
- paragraphCount: paragraphs.length
1491
+ paragraphCount: mcp.paragraphs.length,
1492
+ segmentCount: mcp.paragraphs.reduce((sum, p) => sum + p.segments.length, 0)
1330
1493
  }
1331
1494
  },
1332
1495
  globalOpts
@@ -1335,10 +1498,7 @@ function registerNotesTranscript(parent) {
1335
1498
  }
1336
1499
  const mode = resolveOutputMode(globalOpts);
1337
1500
  if (mode === "json" && format !== "json") {
1338
- printOutput(
1339
- { ok: true, data: { guid: note.guid, paragraphs } },
1340
- globalOpts
1341
- );
1501
+ printOutput({ ok: true, data: mcp }, globalOpts);
1342
1502
  } else {
1343
1503
  process.stdout.write(content);
1344
1504
  }
@@ -1364,7 +1524,7 @@ async function fetchAllParagraphs2(client, guid) {
1364
1524
  }
1365
1525
  return all;
1366
1526
  }
1367
- function pickFormat2(format, output) {
1527
+ function pickFormat2(format, output, globalOpts) {
1368
1528
  const allowed = ["md", "json", "txt"];
1369
1529
  if (format) {
1370
1530
  const f = format.toLowerCase();
@@ -1383,9 +1543,12 @@ function pickFormat2(format, output) {
1383
1543
  if (output) {
1384
1544
  if (output.endsWith(".json")) return "json";
1385
1545
  if (output.endsWith(".md")) return "md";
1386
- return "txt";
1546
+ if (output.endsWith(".txt")) return "txt";
1547
+ return "md";
1387
1548
  }
1388
- return "txt";
1549
+ if (globalOpts.json) return "json";
1550
+ if (globalOpts.pretty) return "md";
1551
+ return process.stdout.isTTY ? "md" : "txt";
1389
1552
  }
1390
1553
 
1391
1554
  // src/commands/notes/index.ts