@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/README.md +54 -33
- package/SPEC.md +2 -2
- package/dist/bin/tiro.js +274 -111
- package/dist/bin/tiro.js.map +1 -1
- package/package.json +2 -1
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
|
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 (
|
|
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
|
|
933
|
-
return Math.min(n,
|
|
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)} ${
|
|
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
|
-
|
|
969
|
-
var
|
|
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 [
|
|
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
|
|
1039
|
-
return Math.min(n,
|
|
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/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
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
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
|
-
|
|
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
|
|
1397
|
+
const first = await client.getJson(
|
|
1252
1398
|
`/v1/external/notes/${guid}/paragraphs`,
|
|
1253
1399
|
ParagraphsCursorSchema.or(ParagraphsListSchema)
|
|
1254
1400
|
);
|
|
1255
|
-
const all = [...
|
|
1256
|
-
if ("nextCursor" in
|
|
1257
|
-
let cursor =
|
|
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
|
|
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
|
|
1314
|
-
|
|
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 "
|
|
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
|