@patimweb/pi-teams 1.0.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/LICENSE +21 -0
- package/README.md +412 -0
- package/index.ts +1 -0
- package/package.json +61 -0
- package/prompts/teams-catch-up.md +24 -0
- package/prompts/teams-channel-digest.md +23 -0
- package/prompts/teams-doctor.md +21 -0
- package/prompts/teams-meeting-prep.md +22 -0
- package/prompts/teams-standup.md +21 -0
- package/prompts/teams-triage.md +22 -0
- package/skills/teams-collaboration/SKILL.md +143 -0
- package/src/auth/client-credentials.ts +72 -0
- package/src/auth/device-code.ts +276 -0
- package/src/auth/index.ts +105 -0
- package/src/auth/jwt.ts +50 -0
- package/src/auth/token-store.ts +156 -0
- package/src/config/index.ts +685 -0
- package/src/config/scope.ts +272 -0
- package/src/extension/index.ts +336 -0
- package/src/graph/calendar.ts +165 -0
- package/src/graph/chats.ts +159 -0
- package/src/graph/client.ts +234 -0
- package/src/graph/mappers.ts +268 -0
- package/src/graph/me.ts +80 -0
- package/src/graph/messages.ts +383 -0
- package/src/graph/presence.ts +160 -0
- package/src/graph/search.ts +111 -0
- package/src/graph/teams.ts +271 -0
- package/src/safety/audit.ts +57 -0
- package/src/safety/index.ts +186 -0
- package/src/status.ts +90 -0
- package/src/tools/resolve.ts +138 -0
- package/src/tools/shared.ts +124 -0
- package/src/tools/teams-accounts.ts +169 -0
- package/src/tools/teams-create-channel.ts +89 -0
- package/src/tools/teams-create-chat.ts +112 -0
- package/src/tools/teams-delete-message.ts +114 -0
- package/src/tools/teams-doctor.ts +182 -0
- package/src/tools/teams-find-user.ts +64 -0
- package/src/tools/teams-inbox.ts +142 -0
- package/src/tools/teams-list-channels.ts +62 -0
- package/src/tools/teams-list-chats.ts +74 -0
- package/src/tools/teams-list-files.ts +54 -0
- package/src/tools/teams-list-members.ts +72 -0
- package/src/tools/teams-list-teams.ts +65 -0
- package/src/tools/teams-login.ts +199 -0
- package/src/tools/teams-meetings.ts +342 -0
- package/src/tools/teams-permissions.ts +169 -0
- package/src/tools/teams-presence.ts +216 -0
- package/src/tools/teams-react.ts +109 -0
- package/src/tools/teams-read-channel.ts +75 -0
- package/src/tools/teams-read-chat.ts +73 -0
- package/src/tools/teams-read-thread.ts +88 -0
- package/src/tools/teams-search-messages.ts +71 -0
- package/src/tools/teams-send-channel-message.ts +250 -0
- package/src/tools/teams-send-chat-message.ts +152 -0
- package/src/tools/teams-setup.ts +245 -0
- package/src/tools/teams-status.ts +73 -0
- package/src/tools/tool-names.ts +30 -0
- package/src/types.ts +192 -0
- package/src/utils/errors.ts +133 -0
- package/src/utils/formatting.ts +356 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure transformation functions: domain data → display strings.
|
|
3
|
+
*
|
|
4
|
+
* No side effects, no I/O. Everything the agent reads about Teams goes through
|
|
5
|
+
* here, so the rules are: never invent content, never silently drop a sender
|
|
6
|
+
* or timestamp, and keep IDs visible so follow-up tool calls have something to
|
|
7
|
+
* quote.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ChannelSummary,
|
|
12
|
+
ChatSummary,
|
|
13
|
+
DriveItemSummary,
|
|
14
|
+
EventSummary,
|
|
15
|
+
MemberSummary,
|
|
16
|
+
MessageSummary,
|
|
17
|
+
PresenceInfo,
|
|
18
|
+
TeamSummary,
|
|
19
|
+
} from "../types.ts";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// HTML → text
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const ENTITIES: Record<string, string> = {
|
|
26
|
+
" ": " ",
|
|
27
|
+
"&": "&",
|
|
28
|
+
"<": "<",
|
|
29
|
+
">": ">",
|
|
30
|
+
""": '"',
|
|
31
|
+
"'": "'",
|
|
32
|
+
"'": "'",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Flatten Teams message HTML into readable plain text.
|
|
37
|
+
*
|
|
38
|
+
* Teams sends most messages as HTML even when the user typed plain text, and
|
|
39
|
+
* an agent reading raw `<div>`s wastes tokens and misreads content. This keeps
|
|
40
|
+
* line structure, mention text and link targets, and drops the rest.
|
|
41
|
+
*/
|
|
42
|
+
export function htmlToText(content: string, contentType = "html"): string {
|
|
43
|
+
if (!content) return "";
|
|
44
|
+
if (contentType.toLowerCase() !== "html") return content.trim();
|
|
45
|
+
|
|
46
|
+
let text = content;
|
|
47
|
+
|
|
48
|
+
// Keep the target of real links: "text (https://…)"
|
|
49
|
+
text = text.replace(
|
|
50
|
+
/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi,
|
|
51
|
+
(_m, href: string, label: string) => {
|
|
52
|
+
const clean = label.replace(/<[^>]+>/g, "").trim();
|
|
53
|
+
if (!clean) return href;
|
|
54
|
+
return clean === href ? href : `${clean} (${href})`;
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Images carry no text; name them so a reader knows something was there.
|
|
59
|
+
text = text.replace(/<img\b[^>]*alt=["']([^"']*)["'][^>]*>/gi, (_m, alt: string) =>
|
|
60
|
+
alt ? `[image: ${alt}]` : "[image]",
|
|
61
|
+
);
|
|
62
|
+
text = text.replace(/<img\b[^>]*>/gi, "[image]");
|
|
63
|
+
|
|
64
|
+
// Block elements become line breaks.
|
|
65
|
+
text = text.replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n");
|
|
66
|
+
text = text.replace(/<br\s*\/?>/gi, "\n");
|
|
67
|
+
text = text.replace(/<li\b[^>]*>/gi, "- ");
|
|
68
|
+
|
|
69
|
+
// Everything else goes.
|
|
70
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
71
|
+
|
|
72
|
+
for (const [entity, replacement] of Object.entries(ENTITIES)) {
|
|
73
|
+
text = text.split(entity).join(replacement);
|
|
74
|
+
}
|
|
75
|
+
text = text.replace(/&#(\d+);/g, (_m, code: string) => String.fromCharCode(Number(code)));
|
|
76
|
+
|
|
77
|
+
return text
|
|
78
|
+
.split("\n")
|
|
79
|
+
.map((line) => line.replace(/[ \t]+/g, " ").trimEnd())
|
|
80
|
+
.join("\n")
|
|
81
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
82
|
+
.trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Convert plain text to the minimal HTML Teams expects for a message body. */
|
|
86
|
+
export function textToHtml(text: string): string {
|
|
87
|
+
const escaped = text
|
|
88
|
+
.replace(/&/g, "&")
|
|
89
|
+
.replace(/</g, "<")
|
|
90
|
+
.replace(/>/g, ">");
|
|
91
|
+
return escaped
|
|
92
|
+
.split(/\n{2,}/)
|
|
93
|
+
.map((para) => `<p>${para.replace(/\n/g, "<br>")}</p>`)
|
|
94
|
+
.join("");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Dates
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/** ISO timestamp → "2026-09-14 08:31" in the local timezone. */
|
|
102
|
+
export function formatDate(iso: string | undefined): string {
|
|
103
|
+
if (!iso) return "";
|
|
104
|
+
const date = new Date(iso);
|
|
105
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
106
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
107
|
+
return (
|
|
108
|
+
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
|
109
|
+
`${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** "3 min ago", "2 h ago", "5 d ago" — for scanning a list quickly. */
|
|
114
|
+
export function formatRelative(iso: string | undefined): string {
|
|
115
|
+
if (!iso) return "";
|
|
116
|
+
const then = new Date(iso).getTime();
|
|
117
|
+
if (Number.isNaN(then)) return "";
|
|
118
|
+
const diffMs = Date.now() - then;
|
|
119
|
+
const minutes = Math.round(diffMs / 60000);
|
|
120
|
+
if (minutes < 1) return "just now";
|
|
121
|
+
if (minutes < 60) return `${minutes} min ago`;
|
|
122
|
+
const hours = Math.round(minutes / 60);
|
|
123
|
+
if (hours < 24) return `${hours} h ago`;
|
|
124
|
+
const days = Math.round(hours / 24);
|
|
125
|
+
if (days < 30) return `${days} d ago`;
|
|
126
|
+
return formatDate(iso);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Shorten a string for previews, without cutting mid-escape. */
|
|
130
|
+
export function truncate(text: string, max: number): string {
|
|
131
|
+
const oneLine = text.replace(/\s*\n\s*/g, " ").trim();
|
|
132
|
+
if (oneLine.length <= max) return oneLine;
|
|
133
|
+
return `${oneLine.slice(0, Math.max(0, max - 1))}…`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function formatBytes(bytes: number | undefined): string {
|
|
137
|
+
if (bytes === undefined) return "";
|
|
138
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
139
|
+
let value = bytes;
|
|
140
|
+
let unit = 0;
|
|
141
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
142
|
+
value /= 1024;
|
|
143
|
+
unit += 1;
|
|
144
|
+
}
|
|
145
|
+
return `${value.toFixed(value < 10 && unit > 0 ? 1 : 0)} ${units[unit]}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Teams & channels
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
export function formatTeamList(teams: readonly TeamSummary[]): string {
|
|
153
|
+
if (teams.length === 0) return "No teams found (or none allowed by your configuration).";
|
|
154
|
+
const lines = [`Found ${teams.length} team(s):`, ""];
|
|
155
|
+
for (const team of teams) {
|
|
156
|
+
const flags = [team.visibility, team.isArchived ? "archived" : undefined]
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.join(", ");
|
|
159
|
+
lines.push(`- **${team.displayName}**${flags ? ` (${flags})` : ""}`);
|
|
160
|
+
if (team.description) lines.push(` ${truncate(team.description, 120)}`);
|
|
161
|
+
lines.push(` id: ${team.id}`);
|
|
162
|
+
}
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function formatChannelList(channels: readonly ChannelSummary[]): string {
|
|
167
|
+
if (channels.length === 0) return "No channels found (or none allowed by your configuration).";
|
|
168
|
+
const lines = [`Found ${channels.length} channel(s):`, ""];
|
|
169
|
+
for (const channel of channels) {
|
|
170
|
+
const path = channel.teamName ? `${channel.teamName}/${channel.displayName}` : channel.displayName;
|
|
171
|
+
lines.push(`- **${path}**${channel.membershipType ? ` (${channel.membershipType})` : ""}`);
|
|
172
|
+
if (channel.description) lines.push(` ${truncate(channel.description, 120)}`);
|
|
173
|
+
lines.push(` channelId: ${channel.id}${channel.teamId ? ` · teamId: ${channel.teamId}` : ""}`);
|
|
174
|
+
}
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function formatMemberList(members: readonly MemberSummary[], title: string): string {
|
|
179
|
+
if (members.length === 0) return `${title}: no members visible.`;
|
|
180
|
+
const lines = [`${title} — ${members.length} member(s):`, ""];
|
|
181
|
+
for (const member of members) {
|
|
182
|
+
const roles = member.roles.length > 0 ? ` [${member.roles.join(", ")}]` : "";
|
|
183
|
+
const contact = member.upn ?? member.mail;
|
|
184
|
+
lines.push(`- ${member.displayName}${contact ? ` <${contact}>` : ""}${roles}`);
|
|
185
|
+
}
|
|
186
|
+
return lines.join("\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Chats
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
export function formatChatList(chats: readonly ChatSummary[]): string {
|
|
194
|
+
if (chats.length === 0) return "No chats found (or none allowed by your configuration).";
|
|
195
|
+
const lines = [`Found ${chats.length} chat(s):`, ""];
|
|
196
|
+
for (const chat of chats) {
|
|
197
|
+
lines.push(`- **${chat.label}** (${chat.chatType})`);
|
|
198
|
+
if (chat.lastMessagePreview) {
|
|
199
|
+
const who = chat.lastMessageFrom ? `${chat.lastMessageFrom}: ` : "";
|
|
200
|
+
lines.push(` ${who}${truncate(chat.lastMessagePreview, 120)}`);
|
|
201
|
+
}
|
|
202
|
+
const when = chat.lastUpdated ? ` · ${formatRelative(chat.lastUpdated)}` : "";
|
|
203
|
+
lines.push(` chatId: ${chat.id}${when}`);
|
|
204
|
+
}
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function formatChatDetail(chat: ChatSummary): string {
|
|
209
|
+
const lines = [`## ${chat.label}`, ""];
|
|
210
|
+
lines.push(`- **Type:** ${chat.chatType}`);
|
|
211
|
+
if (chat.topic) lines.push(`- **Topic:** ${chat.topic}`);
|
|
212
|
+
lines.push(`- **Members:** ${chat.members.map((m) => m.displayName).join(", ") || "(none visible)"}`);
|
|
213
|
+
if (chat.lastUpdated) lines.push(`- **Last activity:** ${formatDate(chat.lastUpdated)}`);
|
|
214
|
+
lines.push(`- **chatId:** ${chat.id}`);
|
|
215
|
+
if (chat.webUrl) lines.push(`- **Open in Teams:** ${chat.webUrl}`);
|
|
216
|
+
return lines.join("\n");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Messages
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
/** One message, rendered for reading. */
|
|
224
|
+
export function formatMessage(message: MessageSummary, options: { showId?: boolean } = {}): string {
|
|
225
|
+
const who = message.from?.displayName ?? "(system)";
|
|
226
|
+
const when = message.createdDateTime ? formatDate(message.createdDateTime) : "";
|
|
227
|
+
const header = `**${who}**${when ? ` · ${when}` : ""}`;
|
|
228
|
+
|
|
229
|
+
const lines = [header];
|
|
230
|
+
if (message.subject) lines.push(`_${message.subject}_`);
|
|
231
|
+
|
|
232
|
+
if (message.deletedDateTime) {
|
|
233
|
+
lines.push("(message deleted)");
|
|
234
|
+
} else {
|
|
235
|
+
lines.push(message.text || "(empty message)");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const meta: string[] = [];
|
|
239
|
+
if (message.reactions.length > 0) {
|
|
240
|
+
const grouped = new Map<string, number>();
|
|
241
|
+
for (const reaction of message.reactions) {
|
|
242
|
+
grouped.set(reaction.type, (grouped.get(reaction.type) ?? 0) + 1);
|
|
243
|
+
}
|
|
244
|
+
meta.push(
|
|
245
|
+
[...grouped.entries()].map(([type, count]) => `${type}${count > 1 ? `×${count}` : ""}`).join(" "),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (message.attachments.length > 0) {
|
|
249
|
+
meta.push(`attachments: ${message.attachments.map((a) => a.name ?? "file").join(", ")}`);
|
|
250
|
+
}
|
|
251
|
+
if (message.replyCount !== undefined && message.replyCount > 0) {
|
|
252
|
+
meta.push(`${message.replyCount} repl${message.replyCount === 1 ? "y" : "ies"}`);
|
|
253
|
+
}
|
|
254
|
+
if (options.showId !== false) meta.push(`messageId: ${message.id}`);
|
|
255
|
+
if (meta.length > 0) lines.push(` _${meta.join(" · ")}_`);
|
|
256
|
+
|
|
257
|
+
return lines.join("\n");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function formatMessageList(
|
|
261
|
+
messages: readonly MessageSummary[],
|
|
262
|
+
title: string,
|
|
263
|
+
options: { oldestFirst?: boolean } = {},
|
|
264
|
+
): string {
|
|
265
|
+
if (messages.length === 0) return `${title}: no messages.`;
|
|
266
|
+
const ordered = options.oldestFirst === false ? [...messages] : [...messages].reverse();
|
|
267
|
+
const lines = [`## ${title}`, "", `${messages.length} message(s), oldest first:`, ""];
|
|
268
|
+
for (const message of ordered) {
|
|
269
|
+
lines.push(formatMessage(message));
|
|
270
|
+
lines.push("");
|
|
271
|
+
}
|
|
272
|
+
return lines.join("\n").trimEnd();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Presence
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
const PRESENCE_ICONS: Record<string, string> = {
|
|
280
|
+
Available: "🟢",
|
|
281
|
+
AvailableIdle: "🟢",
|
|
282
|
+
Away: "🟡",
|
|
283
|
+
BeRightBack: "🟡",
|
|
284
|
+
Busy: "🔴",
|
|
285
|
+
BusyIdle: "🔴",
|
|
286
|
+
DoNotDisturb: "⛔",
|
|
287
|
+
Offline: "⚪",
|
|
288
|
+
PresenceUnknown: "⚪",
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export function formatPresence(presence: PresenceInfo): string {
|
|
292
|
+
const icon = PRESENCE_ICONS[presence.availability] ?? "⚪";
|
|
293
|
+
const name = presence.displayName ? `${presence.displayName}: ` : "";
|
|
294
|
+
const status = presence.statusMessage ? ` — "${truncate(presence.statusMessage, 100)}"` : "";
|
|
295
|
+
return `${icon} ${name}${presence.availability} (${presence.activity})${status}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function formatPresenceList(entries: readonly PresenceInfo[]): string {
|
|
299
|
+
if (entries.length === 0) return "No presence information available.";
|
|
300
|
+
return entries.map((entry) => `- ${formatPresence(entry)}`).join("\n");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Calendar
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
export function formatEventList(events: readonly EventSummary[]): string {
|
|
308
|
+
if (events.length === 0) return "No events in that range.";
|
|
309
|
+
const lines = [`${events.length} event(s):`, ""];
|
|
310
|
+
for (const event of events) {
|
|
311
|
+
const when = event.isAllDay
|
|
312
|
+
? `${formatDate(event.start)} (all day)`
|
|
313
|
+
: `${formatDate(event.start)} – ${formatDate(event.end)?.slice(-5)}`;
|
|
314
|
+
lines.push(`- **${event.subject}** · ${when}`);
|
|
315
|
+
if (event.organizer) lines.push(` organizer: ${event.organizer.displayName}`);
|
|
316
|
+
if (event.attendees.length > 0) {
|
|
317
|
+
lines.push(` attendees: ${truncate(event.attendees.map((a) => a.displayName).join(", "), 140)}`);
|
|
318
|
+
}
|
|
319
|
+
if (event.joinUrl) lines.push(` join: ${event.joinUrl}`);
|
|
320
|
+
lines.push(` eventId: ${event.id}`);
|
|
321
|
+
}
|
|
322
|
+
return lines.join("\n");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function formatEventDetail(event: EventSummary): string {
|
|
326
|
+
const lines = [`## ${event.subject}`, ""];
|
|
327
|
+
lines.push(`- **When:** ${formatDate(event.start)} – ${formatDate(event.end)}${event.timeZone ? ` (${event.timeZone})` : ""}`);
|
|
328
|
+
if (event.location) lines.push(`- **Location:** ${event.location}`);
|
|
329
|
+
if (event.organizer) lines.push(`- **Organizer:** ${event.organizer.displayName}`);
|
|
330
|
+
if (event.attendees.length > 0) {
|
|
331
|
+
lines.push(`- **Attendees:** ${event.attendees.map((a) => a.displayName).join(", ")}`);
|
|
332
|
+
}
|
|
333
|
+
if (event.joinUrl) lines.push(`- **Join:** ${event.joinUrl}`);
|
|
334
|
+
lines.push(`- **eventId:** ${event.id}`);
|
|
335
|
+
if (event.bodyPreview) {
|
|
336
|
+
lines.push("", "### Notes", "", truncate(event.bodyPreview, 600));
|
|
337
|
+
}
|
|
338
|
+
return lines.join("\n");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Files
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
export function formatFileList(items: readonly DriveItemSummary[], title: string): string {
|
|
346
|
+
if (items.length === 0) return `${title}: no files.`;
|
|
347
|
+
const lines = [`## ${title}`, ""];
|
|
348
|
+
for (const item of items) {
|
|
349
|
+
const icon = item.isFolder ? "📁" : "📄";
|
|
350
|
+
const size = item.isFolder ? "" : ` · ${formatBytes(item.size)}`;
|
|
351
|
+
const modified = item.lastModifiedDateTime ? ` · ${formatRelative(item.lastModifiedDateTime)}` : "";
|
|
352
|
+
lines.push(`- ${icon} **${item.name}**${size}${modified}`);
|
|
353
|
+
if (item.webUrl) lines.push(` ${item.webUrl}`);
|
|
354
|
+
}
|
|
355
|
+
return lines.join("\n");
|
|
356
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"lib": ["es2024"],
|
|
5
|
+
"module": "nodenext",
|
|
6
|
+
"moduleResolution": "nodenext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"erasableSyntaxOnly": true,
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["index.ts", "src/**/*.ts", "test/**/*.ts"]
|
|
16
|
+
}
|