@theplato/tiro-cli 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -33
- package/SPEC.md +2 -2
- package/dist/bin/tiro.js +436 -125
- package/dist/bin/tiro.js.map +1 -1
- package/package.json +3 -1
package/dist/bin/tiro.js
CHANGED
|
@@ -175,8 +175,8 @@ async function startLoopbackServer() {
|
|
|
175
175
|
);
|
|
176
176
|
if (pendingResolve) settle(pendingResolve, { code, state });
|
|
177
177
|
});
|
|
178
|
-
await new Promise((
|
|
179
|
-
server.listen(0, "127.0.0.1", () =>
|
|
178
|
+
await new Promise((resolve3) => {
|
|
179
|
+
server.listen(0, "127.0.0.1", () => resolve3());
|
|
180
180
|
});
|
|
181
181
|
const address = server.address();
|
|
182
182
|
const port = address.port;
|
|
@@ -185,7 +185,7 @@ async function startLoopbackServer() {
|
|
|
185
185
|
redirectUri,
|
|
186
186
|
port,
|
|
187
187
|
waitForCallback(timeoutMs) {
|
|
188
|
-
return new Promise((
|
|
188
|
+
return new Promise((resolve3, reject) => {
|
|
189
189
|
const timer = setTimeout(() => {
|
|
190
190
|
pendingResolve = null;
|
|
191
191
|
pendingReject = null;
|
|
@@ -193,7 +193,7 @@ async function startLoopbackServer() {
|
|
|
193
193
|
}, timeoutMs);
|
|
194
194
|
pendingResolve = (r) => {
|
|
195
195
|
clearTimeout(timer);
|
|
196
|
-
|
|
196
|
+
resolve3(r);
|
|
197
197
|
};
|
|
198
198
|
pendingReject = (e) => {
|
|
199
199
|
clearTimeout(timer);
|
|
@@ -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,34 +935,113 @@ 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
|
+
|
|
974
|
+
// src/lib/util/noteFilter.ts
|
|
975
|
+
var PLACEHOLDER_TITLE = "Untitled";
|
|
976
|
+
var PLACEHOLDER_SOURCE_TYPES = /* @__PURE__ */ new Set(["onboarding"]);
|
|
977
|
+
function isVisibleNote(note) {
|
|
978
|
+
if (note.title === PLACEHOLDER_TITLE) return false;
|
|
979
|
+
if (note.sourceType !== null && note.sourceType !== void 0 && PLACEHOLDER_SOURCE_TYPES.has(note.sourceType)) {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
|
|
906
985
|
// src/commands/notes/list.ts
|
|
907
986
|
var ListResponseSchema = PageCursorResponseSchema(NoteSchema);
|
|
987
|
+
var DEFAULT_PAGE_SIZE = 100;
|
|
988
|
+
var MAX_PAGE_SIZE = 1e3;
|
|
989
|
+
var HELP_AFTER = `
|
|
990
|
+
Examples:
|
|
991
|
+
tiro notes list --since 7d
|
|
992
|
+
tiro notes list --keyword "OKR" --since 30d --json
|
|
993
|
+
tiro notes list --folder <id> --limit 50
|
|
994
|
+
|
|
995
|
+
Keyword matching:
|
|
996
|
+
--keyword reorders results by OpenSearch relevance (case-insensitive,
|
|
997
|
+
full-text against note title and paragraph content). When --keyword is
|
|
998
|
+
set, nextCursor is always null. Without --keyword, results are ordered
|
|
999
|
+
by createdAt desc.
|
|
1000
|
+
|
|
1001
|
+
Note: placeholder notes (title='Untitled' or sourceType='onboarding') are
|
|
1002
|
+
filtered out by default. A page of N may return fewer than N visible notes \u2014
|
|
1003
|
+
keep paginating to fetch more, or pass --include-untitled to surface them.
|
|
1004
|
+
`;
|
|
908
1005
|
function registerNotesList(parent) {
|
|
909
|
-
parent.command("list").description("List
|
|
1006
|
+
parent.command("list").description("List notes (lightweight metadata).").option("--keyword <text>", 'Reorder by OpenSearch relevance for this keyword (e.g. "OKR")').option("--folder <id>", "Restrict to a folder and its descendants").option(
|
|
1007
|
+
"--since <date>",
|
|
1008
|
+
"Inclusive lower bound on createdAt (ISO-8601 or relative: 7d, 24h, 30m)"
|
|
1009
|
+
).option("--until <date>", "Exclusive upper bound on createdAt").option(
|
|
1010
|
+
"--limit <n>",
|
|
1011
|
+
`Max results per page (default ${DEFAULT_PAGE_SIZE}, max ${MAX_PAGE_SIZE})`
|
|
1012
|
+
).option("--cursor <token>", "Continue a previous page").option(
|
|
1013
|
+
"--include-untitled",
|
|
1014
|
+
"Include placeholder notes (title='Untitled' or sourceType='onboarding'). Default: hidden",
|
|
1015
|
+
false
|
|
1016
|
+
).addHelpText("after", HELP_AFTER).action(async (opts, cmd) => {
|
|
910
1017
|
const globalOpts = cmd.optsWithGlobals();
|
|
911
|
-
const limit = clampLimit(opts.limit);
|
|
912
1018
|
const client = createApiClient({
|
|
913
1019
|
...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
|
|
914
1020
|
});
|
|
915
1021
|
const params = {};
|
|
1022
|
+
if (opts.keyword) params["keyword"] = opts.keyword;
|
|
916
1023
|
if (opts.folder) params["folderId"] = opts.folder;
|
|
917
|
-
if (
|
|
1024
|
+
if (opts.since) params["createdAtFrom"] = parseDate(opts.since);
|
|
1025
|
+
if (opts.until) params["createdAtTo"] = parseDate(opts.until);
|
|
1026
|
+
const size = clampLimit(opts.limit);
|
|
1027
|
+
if (size !== void 0) params["size"] = size;
|
|
918
1028
|
if (opts.cursor) params["cursor"] = opts.cursor;
|
|
919
1029
|
const res = await client.getJson("/v1/external/notes", ListResponseSchema, params);
|
|
1030
|
+
const visible = opts.includeUntitled === true ? res.content : res.content.filter(isVisibleNote);
|
|
920
1031
|
const mode = resolveOutputMode(globalOpts);
|
|
921
1032
|
if (mode === "json") {
|
|
922
|
-
for (const note of
|
|
1033
|
+
for (const note of visible) printNdjson(note);
|
|
923
1034
|
if (res.nextCursor) printNdjson({ _cursor: res.nextCursor });
|
|
924
1035
|
} else {
|
|
925
|
-
printPretty(
|
|
1036
|
+
printPretty(visible, res.nextCursor, globalOpts);
|
|
926
1037
|
}
|
|
927
1038
|
});
|
|
928
1039
|
}
|
|
929
1040
|
function clampLimit(raw) {
|
|
930
1041
|
if (!raw) return void 0;
|
|
931
1042
|
const n = parseInt(raw, 10);
|
|
932
|
-
if (!Number.isFinite(n) || n <= 0) return
|
|
933
|
-
return Math.min(n,
|
|
1043
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_PAGE_SIZE;
|
|
1044
|
+
return Math.min(n, MAX_PAGE_SIZE);
|
|
934
1045
|
}
|
|
935
1046
|
function printPretty(notes, nextCursor, opts) {
|
|
936
1047
|
if (notes.length === 0) {
|
|
@@ -938,11 +1049,13 @@ function printPretty(notes, nextCursor, opts) {
|
|
|
938
1049
|
`);
|
|
939
1050
|
return;
|
|
940
1051
|
}
|
|
1052
|
+
const titleWidth = computeTitleWidth();
|
|
941
1053
|
for (const n of notes) {
|
|
942
1054
|
const date = n.createdAt.slice(0, 10);
|
|
943
1055
|
const dur = formatDuration(n.recordingDurationSeconds);
|
|
1056
|
+
const title = truncate(n.title, titleWidth);
|
|
944
1057
|
process.stdout.write(
|
|
945
|
-
`${color(date, "gray", opts)} ${color(n.guid, "dim", opts)} ${color(dur, "cyan", opts)} ${
|
|
1058
|
+
`${color(date, "gray", opts)} ${color(n.guid, "dim", opts)} ${color(dur, "cyan", opts)} ${title}
|
|
946
1059
|
`
|
|
947
1060
|
);
|
|
948
1061
|
}
|
|
@@ -954,6 +1067,15 @@ function printPretty(notes, nextCursor, opts) {
|
|
|
954
1067
|
);
|
|
955
1068
|
}
|
|
956
1069
|
}
|
|
1070
|
+
function computeTitleWidth() {
|
|
1071
|
+
const cols = process.stdout.columns;
|
|
1072
|
+
if (!cols || cols < 60) return 40;
|
|
1073
|
+
return Math.max(20, cols - 60);
|
|
1074
|
+
}
|
|
1075
|
+
function truncate(s, max) {
|
|
1076
|
+
if (s.length <= max) return s;
|
|
1077
|
+
return s.slice(0, Math.max(0, max - 1)) + "\u2026";
|
|
1078
|
+
}
|
|
957
1079
|
function formatDuration(sec) {
|
|
958
1080
|
if (!sec || sec <= 0) return "\u2014";
|
|
959
1081
|
const m = Math.floor(sec / 60);
|
|
@@ -964,79 +1086,91 @@ function formatDuration(sec) {
|
|
|
964
1086
|
|
|
965
1087
|
// src/commands/notes/search.ts
|
|
966
1088
|
import "commander";
|
|
1089
|
+
var SearchResponseSchema = PageCursorResponseSchema(NoteSchema).passthrough();
|
|
1090
|
+
var DEFAULT_PAGE_SIZE2 = 100;
|
|
1091
|
+
var MAX_PAGE_SIZE2 = 1e3;
|
|
1092
|
+
var HELP_AFTER2 = `
|
|
1093
|
+
Examples:
|
|
1094
|
+
tiro notes search "Q3 Planning"
|
|
1095
|
+
tiro notes search "Acme Corp" --since 7d --json
|
|
1096
|
+
tiro notes search "release" --since 2026-04-01 --until 2026-05-01
|
|
967
1097
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
}
|
|
1098
|
+
Keyword matching:
|
|
1099
|
+
Full-text against note title + paragraph content via OpenSearch.
|
|
1100
|
+
Case-insensitive. Multi-word keywords are tokenized \u2014 "OKR planning"
|
|
1101
|
+
matches notes containing both terms. The deep search variant (this
|
|
1102
|
+
command) hydrates each result with its primary documents (one-pager,
|
|
1103
|
+
custom) so an MCP/LLM client can read content alongside metadata in
|
|
1104
|
+
one call.
|
|
1003
1105
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1106
|
+
Note: placeholder notes (title='Untitled' or sourceType='onboarding') are
|
|
1107
|
+
filtered out by default. Pass --include-untitled to surface them.
|
|
1108
|
+
`;
|
|
1006
1109
|
function registerNotesSearch(parent) {
|
|
1007
|
-
parent.command("search [
|
|
1110
|
+
parent.command("search [keyword]").description("Deep keyword search \u2014 returns notes hydrated with their primary documents.").option(
|
|
1111
|
+
"--keyword <text>",
|
|
1112
|
+
'Alternative to positional keyword (e.g. --keyword "Q3 Planning")'
|
|
1113
|
+
).option("--folder <id>", "Restrict hits to a folder and its descendants").option(
|
|
1114
|
+
"--since <date>",
|
|
1115
|
+
"Inclusive lower bound on createdAt (ISO-8601 or relative: 7d, 24h, 30m)"
|
|
1116
|
+
).option("--until <date>", "Exclusive upper bound on createdAt").option(
|
|
1117
|
+
"--limit <n>",
|
|
1118
|
+
`Max results per page (default ${DEFAULT_PAGE_SIZE2}, max ${MAX_PAGE_SIZE2})`
|
|
1119
|
+
).option(
|
|
1120
|
+
"--cursor <token>",
|
|
1121
|
+
"Continue a previous page (reserved \u2014 backend currently always null)"
|
|
1122
|
+
).option(
|
|
1123
|
+
"--include-untitled",
|
|
1124
|
+
"Include placeholder notes. Default: hidden",
|
|
1125
|
+
false
|
|
1126
|
+
).addHelpText("after", HELP_AFTER2).action(async (positional, opts, cmd) => {
|
|
1008
1127
|
const globalOpts = cmd.optsWithGlobals();
|
|
1128
|
+
const keyword = (positional ?? opts.keyword ?? "").trim();
|
|
1129
|
+
if (!keyword) {
|
|
1130
|
+
throw new TiroError(
|
|
1131
|
+
{
|
|
1132
|
+
code: "missing_keyword",
|
|
1133
|
+
message: "search requires a keyword (positional or --keyword).",
|
|
1134
|
+
errorType: "bad_request",
|
|
1135
|
+
suggestion: 'tiro notes search "OKR"'
|
|
1136
|
+
},
|
|
1137
|
+
ExitCode.Usage
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
const filter = {};
|
|
1141
|
+
if (opts.folder) filter["folderId"] = opts.folder;
|
|
1142
|
+
if (opts.since) filter["createdAtFrom"] = parseDate(opts.since);
|
|
1143
|
+
if (opts.until) filter["createdAtTo"] = parseDate(opts.until);
|
|
1144
|
+
const pagination = {};
|
|
1145
|
+
const size = clampLimit2(opts.limit);
|
|
1146
|
+
if (size !== void 0) pagination["size"] = size;
|
|
1147
|
+
if (opts.cursor) pagination["cursor"] = opts.cursor;
|
|
1148
|
+
const body = { keyword };
|
|
1149
|
+
if (Object.keys(filter).length > 0) body["filter"] = filter;
|
|
1150
|
+
if (Object.keys(pagination).length > 0) body["pagination"] = pagination;
|
|
1009
1151
|
const client = createApiClient({
|
|
1010
1152
|
...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
|
|
1011
1153
|
});
|
|
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
1154
|
const res = await client.postJson(
|
|
1022
1155
|
"/v1/external/notes/search",
|
|
1023
1156
|
SearchResponseSchema,
|
|
1024
1157
|
body
|
|
1025
1158
|
);
|
|
1159
|
+
const visible = opts.includeUntitled === true ? res.content : res.content.filter(isVisibleNote);
|
|
1026
1160
|
const mode = resolveOutputMode(globalOpts);
|
|
1027
1161
|
if (mode === "json") {
|
|
1028
|
-
for (const note of
|
|
1162
|
+
for (const note of visible) printNdjson(note);
|
|
1029
1163
|
if (res.nextCursor) printNdjson({ _cursor: res.nextCursor });
|
|
1030
1164
|
} else {
|
|
1031
|
-
printPretty2(
|
|
1165
|
+
printPretty2(visible, res.nextCursor, globalOpts);
|
|
1032
1166
|
}
|
|
1033
1167
|
});
|
|
1034
1168
|
}
|
|
1035
1169
|
function clampLimit2(raw) {
|
|
1036
1170
|
if (!raw) return void 0;
|
|
1037
1171
|
const n = parseInt(raw, 10);
|
|
1038
|
-
if (!Number.isFinite(n) || n <= 0) return
|
|
1039
|
-
return Math.min(n,
|
|
1172
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_PAGE_SIZE2;
|
|
1173
|
+
return Math.min(n, MAX_PAGE_SIZE2);
|
|
1040
1174
|
}
|
|
1041
1175
|
function printPretty2(notes, nextCursor, opts) {
|
|
1042
1176
|
if (notes.length === 0) {
|
|
@@ -1098,6 +1232,98 @@ async function fileExists(p) {
|
|
|
1098
1232
|
}
|
|
1099
1233
|
}
|
|
1100
1234
|
|
|
1235
|
+
// src/lib/output/transcript.ts
|
|
1236
|
+
function buildMcpTranscript(note, paragraphs) {
|
|
1237
|
+
return {
|
|
1238
|
+
noteGuid: note.guid,
|
|
1239
|
+
title: note.title,
|
|
1240
|
+
participants: note.participants?.map((p) => p.name || p.email || "").filter((s) => typeof s === "string" && s.length > 0) ?? [],
|
|
1241
|
+
createdAt: note.createdAt,
|
|
1242
|
+
recordingDurationSeconds: note.recordingDurationSeconds,
|
|
1243
|
+
paragraphs: paragraphsToMcp(paragraphs)
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
function paragraphsToMcp(paragraphs) {
|
|
1247
|
+
return paragraphs.map((p) => ({
|
|
1248
|
+
timeFrom: p.timeFrom ?? null,
|
|
1249
|
+
timeTo: p.timeTo ?? null,
|
|
1250
|
+
segments: paragraphToSegments(p)
|
|
1251
|
+
})).filter((p) => p.segments.length > 0);
|
|
1252
|
+
}
|
|
1253
|
+
function paragraphToSegments(p) {
|
|
1254
|
+
const ds = p.diarizedSegments;
|
|
1255
|
+
if (ds && ds.length > 0) {
|
|
1256
|
+
return ds.map((s) => ({
|
|
1257
|
+
content: stripHtml(s.content),
|
|
1258
|
+
speaker: {
|
|
1259
|
+
label: s.speaker.label,
|
|
1260
|
+
name: s.speaker.personName ? stripHtml(s.speaker.personName) : null
|
|
1261
|
+
}
|
|
1262
|
+
})).filter((s) => s.content.length > 0);
|
|
1263
|
+
}
|
|
1264
|
+
const plain = stripHtml(p.transcript?.content ?? "");
|
|
1265
|
+
return plain ? [{ content: plain, speaker: null }] : [];
|
|
1266
|
+
}
|
|
1267
|
+
function renderTranscriptJson(t) {
|
|
1268
|
+
return `${JSON.stringify(t, null, 2)}
|
|
1269
|
+
`;
|
|
1270
|
+
}
|
|
1271
|
+
function renderTranscriptMarkdown(t) {
|
|
1272
|
+
const anchor = anchorTime(t);
|
|
1273
|
+
const lines = [];
|
|
1274
|
+
lines.push(`# ${t.title}`, "");
|
|
1275
|
+
if (t.participants.length > 0) {
|
|
1276
|
+
lines.push(`**Participants**: ${t.participants.join(", ")}`, "");
|
|
1277
|
+
}
|
|
1278
|
+
lines.push("## Transcript", "");
|
|
1279
|
+
for (const p of t.paragraphs) {
|
|
1280
|
+
const ts = elapsed(p.timeFrom, anchor);
|
|
1281
|
+
for (const s of p.segments) {
|
|
1282
|
+
const who = s.speaker?.name ?? s.speaker?.label ?? "Unknown";
|
|
1283
|
+
const tag = ts ? `${who}, ${ts}` : who;
|
|
1284
|
+
lines.push(`**[${tag}]** ${s.content}`);
|
|
1285
|
+
}
|
|
1286
|
+
lines.push("");
|
|
1287
|
+
}
|
|
1288
|
+
return `${lines.join("\n").trimEnd()}
|
|
1289
|
+
`;
|
|
1290
|
+
}
|
|
1291
|
+
function renderTranscriptText(t) {
|
|
1292
|
+
const lines = [];
|
|
1293
|
+
for (const p of t.paragraphs) {
|
|
1294
|
+
for (const s of p.segments) {
|
|
1295
|
+
const who = s.speaker?.name ?? s.speaker?.label ?? "Unknown";
|
|
1296
|
+
lines.push(`[${who}] ${s.content}`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return `${lines.join("\n")}
|
|
1300
|
+
`;
|
|
1301
|
+
}
|
|
1302
|
+
function anchorTime(t) {
|
|
1303
|
+
for (const p of t.paragraphs) {
|
|
1304
|
+
if (p.timeFrom) return p.timeFrom;
|
|
1305
|
+
}
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
function elapsed(currentIso, anchorIso) {
|
|
1309
|
+
if (!currentIso || !anchorIso) return "";
|
|
1310
|
+
const cur = Date.parse(currentIso);
|
|
1311
|
+
const anc = Date.parse(anchorIso);
|
|
1312
|
+
if (!Number.isFinite(cur) || !Number.isFinite(anc)) return "";
|
|
1313
|
+
const seconds = Math.max(0, Math.floor((cur - anc) / 1e3));
|
|
1314
|
+
const h = Math.floor(seconds / 3600);
|
|
1315
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
1316
|
+
const s = seconds % 60;
|
|
1317
|
+
if (h > 0) return `${pad(h)}:${pad(m)}:${pad(s)}`;
|
|
1318
|
+
return `${pad(m)}:${pad(s)}`;
|
|
1319
|
+
}
|
|
1320
|
+
function pad(n) {
|
|
1321
|
+
return n.toString().padStart(2, "0");
|
|
1322
|
+
}
|
|
1323
|
+
function stripHtml(s) {
|
|
1324
|
+
return s.replace(/<[^>]*>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1101
1327
|
// src/lib/output/format.ts
|
|
1102
1328
|
function formatNote(note, format, opts = {}) {
|
|
1103
1329
|
switch (format) {
|
|
@@ -1134,20 +1360,19 @@ function formatMarkdown(note, opts) {
|
|
|
1134
1360
|
parts.push("");
|
|
1135
1361
|
}
|
|
1136
1362
|
if (opts.includeTranscript && opts.paragraphs && opts.paragraphs.length > 0) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
parts.push("");
|
|
1143
|
-
}
|
|
1363
|
+
const mcp = buildMcpTranscript(note, opts.paragraphs);
|
|
1364
|
+
const transcriptBody = renderTranscriptMarkdown(mcp);
|
|
1365
|
+
const startsWithHeader = transcriptBody.startsWith(`# ${note.title}`);
|
|
1366
|
+
const trimmed = startsWithHeader ? transcriptBody.slice(transcriptBody.indexOf("\n") + 1) : transcriptBody;
|
|
1367
|
+
parts.push(trimmed.replace(/^\s*\n+/, ""));
|
|
1144
1368
|
}
|
|
1145
|
-
return parts.join("\n")
|
|
1369
|
+
return `${parts.join("\n").trimEnd()}
|
|
1370
|
+
`;
|
|
1146
1371
|
}
|
|
1147
1372
|
function formatJson(note, opts) {
|
|
1148
1373
|
const out = { ...note };
|
|
1149
1374
|
if (opts.includeTranscript && opts.paragraphs) {
|
|
1150
|
-
out["transcript"] = { paragraphs: opts.paragraphs };
|
|
1375
|
+
out["transcript"] = { paragraphs: paragraphsToMcp(opts.paragraphs) };
|
|
1151
1376
|
}
|
|
1152
1377
|
return `${JSON.stringify(out, null, 2)}
|
|
1153
1378
|
`;
|
|
@@ -1158,21 +1383,8 @@ function formatText(note, opts) {
|
|
|
1158
1383
|
${note.webUrl}
|
|
1159
1384
|
`;
|
|
1160
1385
|
}
|
|
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");
|
|
1386
|
+
const mcp = buildMcpTranscript(note, opts.paragraphs);
|
|
1387
|
+
return renderTranscriptText(mcp);
|
|
1176
1388
|
}
|
|
1177
1389
|
function escapeYaml(s) {
|
|
1178
1390
|
if (/[:#\n"']/.test(s)) {
|
|
@@ -1182,20 +1394,35 @@ function escapeYaml(s) {
|
|
|
1182
1394
|
}
|
|
1183
1395
|
|
|
1184
1396
|
// src/commands/notes/get.ts
|
|
1397
|
+
var ALLOWED_INCLUDES = /* @__PURE__ */ new Set(["transcript"]);
|
|
1185
1398
|
var ParagraphsListSchema = SimpleListResponseSchema(ParagraphSchema);
|
|
1186
1399
|
var ParagraphsCursorSchema = PageCursorResponseSchema(ParagraphSchema);
|
|
1187
1400
|
function registerNotesGet(parent) {
|
|
1188
|
-
parent.command("get <guid>").description("Get a single note
|
|
1401
|
+
parent.command("get <guid>").description("Get a single note. Outputs to stdout, or saves to a file with --output.").option("--output <path>", "Write to file (stdout becomes a single metadata line)").option(
|
|
1402
|
+
"--format <md|json|txt>",
|
|
1403
|
+
"Output format (default: md for TTY, json when piped)"
|
|
1404
|
+
).option(
|
|
1189
1405
|
"--include <items>",
|
|
1190
|
-
"Comma-separated extras: transcript",
|
|
1406
|
+
"Comma-separated extras (v0.2 supports: transcript)",
|
|
1191
1407
|
""
|
|
1192
|
-
).option("--force", "Overwrite existing file").
|
|
1408
|
+
).option("--force", "Overwrite existing file at --output path").addHelpText("after", `
|
|
1409
|
+
Examples:
|
|
1410
|
+
tiro notes get <guid> # markdown to stdout
|
|
1411
|
+
tiro notes get <guid> --include transcript # add speaker-attributed paragraphs
|
|
1412
|
+
tiro notes get <guid> --output ./meeting.md --include transcript
|
|
1413
|
+
tiro notes get <guid> --format json # JSON to stdout
|
|
1414
|
+
|
|
1415
|
+
Tip for agents: prefer --output <path>. The actual content goes to disk
|
|
1416
|
+
and stdout collapses to a single metadata line, keeping your context
|
|
1417
|
+
window light.
|
|
1418
|
+
`).action(async (guid, opts, cmd) => {
|
|
1193
1419
|
const globalOpts = cmd.optsWithGlobals();
|
|
1420
|
+
const includes = parseIncludes(opts.include);
|
|
1421
|
+
validateIncludes(includes);
|
|
1422
|
+
const format = pickFormat(opts.format, opts.output);
|
|
1194
1423
|
const client = createApiClient({
|
|
1195
1424
|
...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
|
|
1196
1425
|
});
|
|
1197
|
-
const includes = parseIncludes(opts.include);
|
|
1198
|
-
const format = pickFormat(opts.format, opts.output);
|
|
1199
1426
|
const note = await client.getJson(`/v1/external/notes/${guid}`, NoteSchema);
|
|
1200
1427
|
let paragraphs;
|
|
1201
1428
|
if (includes.has("transcript") || format === "txt") {
|
|
@@ -1231,7 +1458,7 @@ function registerNotesGet(parent) {
|
|
|
1231
1458
|
ok: true,
|
|
1232
1459
|
data: {
|
|
1233
1460
|
...note,
|
|
1234
|
-
...paragraphs && { transcript: { paragraphs } }
|
|
1461
|
+
...paragraphs && { transcript: { paragraphs: paragraphsToMcp(paragraphs) } }
|
|
1235
1462
|
}
|
|
1236
1463
|
},
|
|
1237
1464
|
globalOpts
|
|
@@ -1239,8 +1466,9 @@ function registerNotesGet(parent) {
|
|
|
1239
1466
|
} else if (format === "json") {
|
|
1240
1467
|
process.stdout.write(content);
|
|
1241
1468
|
} else {
|
|
1242
|
-
if (process.stdout.isTTY) {
|
|
1469
|
+
if (process.stdout.isTTY && format === "txt") {
|
|
1243
1470
|
process.stdout.write(`${color(`# ${note.title}`, "bold", globalOpts)}
|
|
1471
|
+
|
|
1244
1472
|
`);
|
|
1245
1473
|
}
|
|
1246
1474
|
process.stdout.write(content);
|
|
@@ -1248,13 +1476,13 @@ function registerNotesGet(parent) {
|
|
|
1248
1476
|
});
|
|
1249
1477
|
}
|
|
1250
1478
|
async function fetchAllParagraphs(client, guid) {
|
|
1251
|
-
const
|
|
1479
|
+
const first = await client.getJson(
|
|
1252
1480
|
`/v1/external/notes/${guid}/paragraphs`,
|
|
1253
1481
|
ParagraphsCursorSchema.or(ParagraphsListSchema)
|
|
1254
1482
|
);
|
|
1255
|
-
const all = [...
|
|
1256
|
-
if ("nextCursor" in
|
|
1257
|
-
let cursor =
|
|
1483
|
+
const all = [...first.content];
|
|
1484
|
+
if ("nextCursor" in first) {
|
|
1485
|
+
let cursor = first.nextCursor;
|
|
1258
1486
|
while (cursor) {
|
|
1259
1487
|
const next = await client.getJson(
|
|
1260
1488
|
`/v1/external/notes/${guid}/paragraphs`,
|
|
@@ -1273,6 +1501,21 @@ function parseIncludes(raw) {
|
|
|
1273
1501
|
raw.split(",").map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0)
|
|
1274
1502
|
);
|
|
1275
1503
|
}
|
|
1504
|
+
function validateIncludes(includes) {
|
|
1505
|
+
for (const inc of includes) {
|
|
1506
|
+
if (!ALLOWED_INCLUDES.has(inc)) {
|
|
1507
|
+
throw new TiroError(
|
|
1508
|
+
{
|
|
1509
|
+
code: "invalid_include",
|
|
1510
|
+
message: `Invalid --include "${inc}". v0.2.0 supports: transcript.`,
|
|
1511
|
+
errorType: "bad_request",
|
|
1512
|
+
suggestion: "Use --include transcript"
|
|
1513
|
+
},
|
|
1514
|
+
ExitCode.Usage
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1276
1519
|
function pickFormat(format, output) {
|
|
1277
1520
|
const allowed = ["md", "json", "txt"];
|
|
1278
1521
|
if (format) {
|
|
@@ -1302,18 +1545,30 @@ import "commander";
|
|
|
1302
1545
|
var ParagraphsListSchema2 = SimpleListResponseSchema(ParagraphSchema);
|
|
1303
1546
|
var ParagraphsCursorSchema2 = PageCursorResponseSchema(ParagraphSchema);
|
|
1304
1547
|
function registerNotesTranscript(parent) {
|
|
1305
|
-
parent.command("transcript <guid>").description(
|
|
1548
|
+
parent.command("transcript <guid>").description(
|
|
1549
|
+
"Get the full transcript of a note as speaker-attributed paragraphs.\nJSON output matches MCP get_note_transcript shape exactly."
|
|
1550
|
+
).option("--output <path>", "Write to file (stdout = single metadata line)").option(
|
|
1551
|
+
"--format <md|json|txt>",
|
|
1552
|
+
"Output format (default: md if TTY, txt when piped; json mirrors MCP)"
|
|
1553
|
+
).option("--force", "Overwrite existing file at --output path").addHelpText("after", `
|
|
1554
|
+
Examples:
|
|
1555
|
+
tiro notes transcript <guid> # md in TTY, txt in pipe
|
|
1556
|
+
tiro notes transcript <guid> --format md --output ./t.md
|
|
1557
|
+
tiro notes transcript <guid> --format json # MCP-shape JSON
|
|
1558
|
+
tiro notes transcript <guid> --format txt --output ./embed.txt
|
|
1559
|
+
|
|
1560
|
+
The --format json output is byte-for-byte identical to MCP's
|
|
1561
|
+
get_note_transcript so agents can swap surfaces without changing parsers.
|
|
1562
|
+
`).action(async (guid, opts, cmd) => {
|
|
1306
1563
|
const globalOpts = cmd.optsWithGlobals();
|
|
1564
|
+
const format = pickFormat2(opts.format, opts.output, globalOpts);
|
|
1307
1565
|
const client = createApiClient({
|
|
1308
1566
|
...globalOpts.hostname !== void 0 && { hostnameOverride: globalOpts.hostname }
|
|
1309
1567
|
});
|
|
1310
|
-
const format = pickFormat2(opts.format, opts.output);
|
|
1311
1568
|
const note = await client.getJson(`/v1/external/notes/${guid}`, NoteSchema);
|
|
1312
1569
|
const paragraphs = await fetchAllParagraphs2(client, guid);
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
paragraphs
|
|
1316
|
-
});
|
|
1570
|
+
const mcp = buildMcpTranscript(note, paragraphs);
|
|
1571
|
+
const content = format === "json" ? renderTranscriptJson(mcp) : format === "md" ? renderTranscriptMarkdown(mcp) : renderTranscriptText(mcp);
|
|
1317
1572
|
if (opts.output) {
|
|
1318
1573
|
const result = await writeFileAtomic(opts.output, content, {
|
|
1319
1574
|
...opts.force === true && { force: true }
|
|
@@ -1326,7 +1581,8 @@ function registerNotesTranscript(parent) {
|
|
|
1326
1581
|
size: result.size,
|
|
1327
1582
|
format,
|
|
1328
1583
|
guid: note.guid,
|
|
1329
|
-
paragraphCount: paragraphs.length
|
|
1584
|
+
paragraphCount: mcp.paragraphs.length,
|
|
1585
|
+
segmentCount: mcp.paragraphs.reduce((sum, p) => sum + p.segments.length, 0)
|
|
1330
1586
|
}
|
|
1331
1587
|
},
|
|
1332
1588
|
globalOpts
|
|
@@ -1335,10 +1591,7 @@ function registerNotesTranscript(parent) {
|
|
|
1335
1591
|
}
|
|
1336
1592
|
const mode = resolveOutputMode(globalOpts);
|
|
1337
1593
|
if (mode === "json" && format !== "json") {
|
|
1338
|
-
printOutput(
|
|
1339
|
-
{ ok: true, data: { guid: note.guid, paragraphs } },
|
|
1340
|
-
globalOpts
|
|
1341
|
-
);
|
|
1594
|
+
printOutput({ ok: true, data: mcp }, globalOpts);
|
|
1342
1595
|
} else {
|
|
1343
1596
|
process.stdout.write(content);
|
|
1344
1597
|
}
|
|
@@ -1364,7 +1617,7 @@ async function fetchAllParagraphs2(client, guid) {
|
|
|
1364
1617
|
}
|
|
1365
1618
|
return all;
|
|
1366
1619
|
}
|
|
1367
|
-
function pickFormat2(format, output) {
|
|
1620
|
+
function pickFormat2(format, output, globalOpts) {
|
|
1368
1621
|
const allowed = ["md", "json", "txt"];
|
|
1369
1622
|
if (format) {
|
|
1370
1623
|
const f = format.toLowerCase();
|
|
@@ -1383,9 +1636,12 @@ function pickFormat2(format, output) {
|
|
|
1383
1636
|
if (output) {
|
|
1384
1637
|
if (output.endsWith(".json")) return "json";
|
|
1385
1638
|
if (output.endsWith(".md")) return "md";
|
|
1386
|
-
return "txt";
|
|
1639
|
+
if (output.endsWith(".txt")) return "txt";
|
|
1640
|
+
return "md";
|
|
1387
1641
|
}
|
|
1388
|
-
return "
|
|
1642
|
+
if (globalOpts.json) return "json";
|
|
1643
|
+
if (globalOpts.pretty) return "md";
|
|
1644
|
+
return process.stdout.isTTY ? "md" : "txt";
|
|
1389
1645
|
}
|
|
1390
1646
|
|
|
1391
1647
|
// src/commands/notes/index.ts
|
|
@@ -1397,21 +1653,74 @@ function registerNotes(program) {
|
|
|
1397
1653
|
registerNotesTranscript(notes);
|
|
1398
1654
|
}
|
|
1399
1655
|
|
|
1656
|
+
// src/lib/updateCheck.ts
|
|
1657
|
+
import updateNotifier from "update-notifier";
|
|
1658
|
+
import { readFile } from "fs/promises";
|
|
1659
|
+
import { fileURLToPath } from "url";
|
|
1660
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
1661
|
+
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
1662
|
+
var CANDIDATE_PATHS = [
|
|
1663
|
+
resolve2(HERE, "../../package.json"),
|
|
1664
|
+
resolve2(HERE, "../../../package.json")
|
|
1665
|
+
];
|
|
1666
|
+
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1667
|
+
async function startUpdateCheck() {
|
|
1668
|
+
if (process.env["NO_UPDATE_NOTIFIER"] === "1") return null;
|
|
1669
|
+
if (process.env["CI"]) return null;
|
|
1670
|
+
if (process.stdout.isTTY !== true) return null;
|
|
1671
|
+
const pkg = await loadPkg();
|
|
1672
|
+
if (!pkg) return null;
|
|
1673
|
+
try {
|
|
1674
|
+
const factory = updateNotifier;
|
|
1675
|
+
return factory({
|
|
1676
|
+
pkg,
|
|
1677
|
+
updateCheckInterval: ONE_DAY_MS,
|
|
1678
|
+
shouldNotifyInNpmScript: false
|
|
1679
|
+
});
|
|
1680
|
+
} catch {
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
function emitUpdateBanner(notifier) {
|
|
1685
|
+
if (!notifier) return;
|
|
1686
|
+
if (!notifier.update) return;
|
|
1687
|
+
notifier.notify({
|
|
1688
|
+
isGlobal: true,
|
|
1689
|
+
defer: false,
|
|
1690
|
+
message: "Update available {currentVersion} \u2192 {latestVersion}\nRun npm install -g {packageName} to update\n\nChangelog: https://www.npmjs.com/package/{packageName}?activeTab=versions"
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
async function loadPkg() {
|
|
1694
|
+
for (const path of CANDIDATE_PATHS) {
|
|
1695
|
+
try {
|
|
1696
|
+
const raw = await readFile(path, "utf8");
|
|
1697
|
+
const parsed = JSON.parse(raw);
|
|
1698
|
+
if (typeof parsed.name === "string" && typeof parsed.version === "string" && parsed.name.length > 0 && parsed.version.length > 0) {
|
|
1699
|
+
return { name: parsed.name, version: parsed.version };
|
|
1700
|
+
}
|
|
1701
|
+
} catch {
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
return null;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1400
1707
|
// src/bin/tiro.ts
|
|
1401
1708
|
var EXAMPLES = `
|
|
1402
1709
|
EXAMPLES
|
|
1403
1710
|
$ tiro auth login
|
|
1404
|
-
$ tiro notes
|
|
1711
|
+
$ tiro notes list --since 7d
|
|
1712
|
+
$ tiro notes search "Q3 Planning" --since 30d --json
|
|
1405
1713
|
$ tiro notes get <guid> --output ./meeting.md --include transcript
|
|
1406
|
-
$ tiro notes transcript <guid> --format
|
|
1714
|
+
$ tiro notes transcript <guid> --format md --output ./transcript.md
|
|
1407
1715
|
|
|
1408
1716
|
ENVIRONMENT
|
|
1409
|
-
TIRO_TOKEN
|
|
1410
|
-
TIRO_HOSTNAME
|
|
1411
|
-
NO_COLOR
|
|
1717
|
+
TIRO_TOKEN Bearer token (overrides keychain \u2014 for CI / agents)
|
|
1718
|
+
TIRO_HOSTNAME API base URL (default: https://api.tiro.ooo)
|
|
1719
|
+
NO_COLOR Disable colors (https://no-color.org)
|
|
1720
|
+
NO_UPDATE_NOTIFIER Set to "1" to disable the update banner
|
|
1412
1721
|
|
|
1413
1722
|
DOCS
|
|
1414
|
-
https://api.tiro.ooo/cli
|
|
1723
|
+
https://api-docs.tiro.ooo/cli/overview
|
|
1415
1724
|
`;
|
|
1416
1725
|
function buildProgram() {
|
|
1417
1726
|
const program = new Command10();
|
|
@@ -1423,8 +1732,10 @@ function buildProgram() {
|
|
|
1423
1732
|
}
|
|
1424
1733
|
async function main() {
|
|
1425
1734
|
const program = buildProgram();
|
|
1735
|
+
const notifier = await startUpdateCheck();
|
|
1426
1736
|
try {
|
|
1427
1737
|
await program.parseAsync(process.argv);
|
|
1738
|
+
emitUpdateBanner(notifier);
|
|
1428
1739
|
} catch (err) {
|
|
1429
1740
|
handleError(err, program);
|
|
1430
1741
|
}
|