@michaelschnyder/teams-cli 0.1.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/cli.js ADDED
@@ -0,0 +1,647 @@
1
+ #!/usr/bin/env node
2
+ import { Argument, Command, Option } from "commander";
3
+ import { realpathSync } from "node:fs";
4
+ import { randomInt, randomUUID } from "node:crypto";
5
+ import { fileURLToPath } from "node:url";
6
+ import { stringify } from "yaml";
7
+ import { describeSession, login, logout, refreshTokens, validateSession, } from "./auth.js";
8
+ import { withDataSession } from "./data.js";
9
+ import { clearStatus, configureDiagnostics, showStatus } from "./diagnostics.js";
10
+ import { activatePolicy, initializePolicy, loadPolicyStore, policyProtectionInstruction, policyStatusWarnings, requireMessageSend, requirePolicyIdentity, requireRawTokenExport, resolvePolicies, resolvePolicyByName, } from "./policy.js";
11
+ import { loadProfiles, removeProfile, requireRuntimeIdentity, resolveRuntimeContext, saveProfile, } from "./config.js";
12
+ import { decodeJwtClaims, formatDuration, readJwtMetadata, secondsUntil } from "./jwt.js";
13
+ import { loadSession, requireCurrentSession, storagePaths, } from "./storage.js";
14
+ import { getChannel, getChat, getMessage, getPerson, getPersonImage, listChannels, listChats, listMessages, sendMessage, searchPeople, } from "./teams-client.js";
15
+ import { registerSkillsCommand } from "./commands/skills.js";
16
+ import { registerVersionCommand } from "./commands/version.js";
17
+ import { prepareUpdateNotification, runUpdateWorker } from "./update.js";
18
+ import { CLI_VERSION } from "./version.js";
19
+ function policyWarningReporter() {
20
+ const reported = new Set();
21
+ return (warnings) => {
22
+ for (const warning of warnings) {
23
+ if (reported.has(warning))
24
+ continue;
25
+ reported.add(warning);
26
+ process.stderr.write(`Warning: ${warning}.\n`);
27
+ }
28
+ };
29
+ }
30
+ async function runtimeContext(program, paths) {
31
+ return resolveRuntimeContext(paths, program.opts());
32
+ }
33
+ async function authorizedRuntime(program, paths, subjectStart, reportPolicyWarnings = policyWarningReporter()) {
34
+ const context = await runtimeContext(program, paths);
35
+ const identity = requireRuntimeIdentity(context);
36
+ const policies = await resolvePolicies(paths, subjectStart);
37
+ reportPolicyWarnings(policyStatusWarnings(policies));
38
+ reportPolicyWarnings(requirePolicyIdentity(policies, identity));
39
+ return { context, identity, policies, reportPolicyWarnings };
40
+ }
41
+ async function withAuthorizedDataSession(program, paths, subjectStart, targets, operation) {
42
+ const runtime = await authorizedRuntime(program, paths, subjectStart);
43
+ return withDataSession(paths, runtime.identity, runtime.context.browser, targets, (session) => operation(session, runtime));
44
+ }
45
+ function outputWhoami(result) {
46
+ const user = result.user;
47
+ process.stdout.write("Authenticated: yes\n");
48
+ process.stdout.write(`Name: ${user.name ?? "unknown"}\n`);
49
+ process.stdout.write(`Username: ${user.username ?? "unknown"}\n`);
50
+ process.stdout.write(`User ID: ${user.id ?? "unknown"}\n`);
51
+ process.stdout.write(`Tenant ID: ${user.tenantId}\n`);
52
+ for (const [label, token] of [
53
+ ["Access token", result.tokens.accessToken],
54
+ ["Skype token", result.tokens.skypeToken],
55
+ ["Chat token", result.tokens.chatToken],
56
+ ["Search token", result.tokens.searchToken],
57
+ ]) {
58
+ process.stdout.write(`${label} audience: ${token.audience ?? "unknown"}\n`);
59
+ process.stdout.write(` Expires: ${token.expiresAt} (${formatDuration(token.expiresInSeconds)} remaining)\n`);
60
+ }
61
+ }
62
+ function selectedTokens(session, target) {
63
+ const all = {
64
+ access: session.accessToken.value,
65
+ skype: session.skypeToken.value,
66
+ chat: session.chatToken.value,
67
+ search: session.searchToken.value,
68
+ };
69
+ return target === "all" ? all : { [target]: all[target] };
70
+ }
71
+ export function renderTokens(session, target, decode) {
72
+ const selected = selectedTokens(session, target);
73
+ if (decode) {
74
+ const claims = Object.fromEntries(Object.entries(selected).map(([name, token]) => [name, decodeJwtClaims(token)]));
75
+ const output = target === "all" ? claims : Object.values(claims)[0];
76
+ return `${JSON.stringify(output, null, 2)}\n`;
77
+ }
78
+ if (target !== "all")
79
+ return `${Object.values(selected)[0]}\n`;
80
+ return [
81
+ `Access token:\n${selected.access}`,
82
+ `Skype token:\n${selected.skype}`,
83
+ `Chat token:\n${selected.chat}`,
84
+ `Search token:\n${selected.search}`,
85
+ ].join("\n\n") + "\n";
86
+ }
87
+ function storedToken(session, target) {
88
+ if (target === "access")
89
+ return session.accessToken;
90
+ if (target === "skype")
91
+ return session.skypeToken;
92
+ return target === "chat" ? session.chatToken : session.searchToken;
93
+ }
94
+ function describeStoredToken(token, now) {
95
+ return {
96
+ value: token.value,
97
+ audience: readJwtMetadata(token.value).audience ?? null,
98
+ expiresAt: token.expiresAt,
99
+ expiresInSeconds: secondsUntil(token.expiresAt, now),
100
+ };
101
+ }
102
+ export function renderRefreshResult(result, now = new Date()) {
103
+ const targets = result.target === "all"
104
+ ? ["access", "skype", "chat", "search"]
105
+ : [result.target];
106
+ const labels = {
107
+ access: "Access token",
108
+ skype: "Skype token",
109
+ chat: "Chat token",
110
+ search: "Search token",
111
+ };
112
+ const lines = [
113
+ `Refreshed ${result.target === "all" ? "all Teams tokens" : `${result.target} token`}.`,
114
+ ];
115
+ for (const target of targets) {
116
+ const previousToken = storedToken(result.before, target);
117
+ const currentToken = storedToken(result.after, target);
118
+ const current = describeStoredToken(currentToken, now);
119
+ lines.push(`${labels[target]}:`);
120
+ const previous = describeStoredToken(previousToken, now);
121
+ lines.push(` Before audience: ${previous.audience ?? "unknown"}`, ` Before expiry: ${previous.expiresAt} (${formatDuration(previous.expiresInSeconds)} remaining)`, ` After audience: ${current.audience ?? "unknown"}`, ` After expiry: ${current.expiresAt} (${formatDuration(current.expiresInSeconds)} remaining)`);
122
+ }
123
+ return `${lines.join("\n")}\n`;
124
+ }
125
+ function fitCell(value, maximum) {
126
+ const normalized = value.replaceAll(/\s+/g, " ").trim();
127
+ return normalized.length <= maximum ? normalized : `${normalized.slice(0, maximum - 1)}…`;
128
+ }
129
+ function formatTimestamp(value) {
130
+ if (!value)
131
+ return "";
132
+ return value
133
+ .replace(/(T\d{2}:\d{2}:\d{2})\.\d+(Z|[+-]\d{2}:\d{2})?$/, "$1$2")
134
+ .replace("T", " ")
135
+ .replace(/Z$/, "");
136
+ }
137
+ function renderTable(rows, headers) {
138
+ const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)));
139
+ const line = (values) => `| ${values.map((value, index) => value.padEnd(widths[index] ?? 0)).join(" | ")} |`;
140
+ return [line(headers), line(widths.map((width) => "-".repeat(width))), ...rows.map(line)];
141
+ }
142
+ function renderChats(result) {
143
+ const rows = result.chats.map((chat) => {
144
+ const returnedNames = chat.participants.map((participant) => participant.displayName ?? participant.id);
145
+ const missing = Math.max(0, chat.participantCount - chat.participants.length - 1);
146
+ const missingLabel = missing > 1 ? ` (+${missing} not returned)` : "";
147
+ const participantText = returnedNames.length
148
+ ? `${returnedNames.join(", ")}${missingLabel}`
149
+ : `none returned${missing > 1 ? ` (${missing} not returned)` : ""}`;
150
+ return [
151
+ fitCell(chat.title, 40),
152
+ fitCell(participantText, 64),
153
+ formatTimestamp(chat.lastActivity),
154
+ chat.id,
155
+ ];
156
+ });
157
+ const lines = [`Chats (${result.chats.length})`, ...renderTable(rows, ["Chat", "Participants", "Last activity", "Chat ID"])];
158
+ if (result.chats.length === 0) {
159
+ lines.splice(1, 2);
160
+ }
161
+ if (result.page.nextCursor)
162
+ lines.push(`Next cursor: ${result.page.nextCursor}`);
163
+ return `${lines.join("\n")}\n`;
164
+ }
165
+ function renderChatResult(result) {
166
+ const chat = result.chat;
167
+ return `${chat.title}\nChat ID: ${chat.id}\nParticipants: ${chat.participants.map((participant) => participant.displayName ?? participant.id).join(", ")}\n`;
168
+ }
169
+ function renderChannels(result) {
170
+ const rows = result.channels.map((channel) => [
171
+ fitCell(channel.name, 40),
172
+ fitCell(channel.team.name, 40),
173
+ channel.id,
174
+ ]);
175
+ const lines = [`Channels (${result.channels.length})`];
176
+ if (rows.length)
177
+ lines.push(...renderTable(rows, ["Channel", "Team", "Channel ID"]));
178
+ return `${lines.join("\n")}\n`;
179
+ }
180
+ function renderChannelResult(result) {
181
+ const channel = result.channel;
182
+ return `${channel.name}\nChannel ID: ${channel.id}\nTeam: ${channel.team.name} (${channel.team.id})\nDescription: ${channel.description ?? ""}\n`;
183
+ }
184
+ function renderMessage(message) {
185
+ const sender = message.sender.displayName ?? message.sender.id ?? "unknown";
186
+ return [
187
+ `- ${message.composedAt ?? message.originalArrivalAt ?? "unknown time"} ${sender} [${message.id}]`,
188
+ ` ${message.content ?? ""}`,
189
+ ];
190
+ }
191
+ function renderMessages(result) {
192
+ const lines = [`Messages (${result.messages.length}) for ${result.target.kind} ${result.target.id}`];
193
+ for (const message of result.messages)
194
+ lines.push(...renderMessage(message));
195
+ if (result.page.nextCursor)
196
+ lines.push(`Next cursor: ${result.page.nextCursor}`);
197
+ return `${lines.join("\n")}\n`;
198
+ }
199
+ function renderMessageResult(result) {
200
+ return `${[`Message for ${result.target.kind} ${result.target.id}`, ...renderMessage(result.message)].join("\n")}\n`;
201
+ }
202
+ function renderMessageSendResult(result) {
203
+ const identifier = result.message ? ` ${result.message.id}` : "";
204
+ return `Sent message${identifier} to ${result.target.kind} ${result.target.id}.\n`;
205
+ }
206
+ export function renderPeople(result) {
207
+ const rows = result.people.map((person) => [
208
+ fitCell(person.displayName ?? "", 40),
209
+ fitCell(person.jobTitle ?? "", 40),
210
+ fitCell(person.email ?? "", 48),
211
+ person.id,
212
+ ]);
213
+ const lines = [`People (${result.people.length})`];
214
+ if (rows.length)
215
+ lines.push(...renderTable(rows, ["Name", "Job title", "Email", "Person ID"]));
216
+ return `${lines.join("\n")}\n`;
217
+ }
218
+ export function renderPerson(result) {
219
+ const person = result.person;
220
+ const phones = person.phones.map((phone) => `${phone.type ? `${phone.type}: ` : ""}${phone.number}`);
221
+ return `${[
222
+ person.displayName ?? "unknown",
223
+ `Person ID: ${person.id}`,
224
+ `MRI: ${person.mri ?? ""}`,
225
+ `Given name: ${person.givenName ?? ""}`,
226
+ `Surname: ${person.surname ?? ""}`,
227
+ `Email: ${person.email ?? ""}`,
228
+ `Mail: ${person.mail ?? ""}`,
229
+ `User principal name: ${person.userPrincipalName ?? ""}`,
230
+ `SMTP addresses: ${person.smtpAddresses.join(", ")}`,
231
+ `Job title: ${person.jobTitle ?? ""}`,
232
+ `Department: ${person.department ?? ""}`,
233
+ `Office: ${person.officeLocation ?? ""}`,
234
+ `Mobile: ${person.mobile ?? ""}`,
235
+ `Telephone: ${person.telephoneNumber ?? ""}`,
236
+ `Phones: ${phones.join(", ")}`,
237
+ `Tenant: ${person.tenantName ?? ""}`,
238
+ `User type: ${person.userType ?? ""}`,
239
+ `Account enabled: ${person.accountEnabled === null ? "unknown" : String(person.accountEnabled)}`,
240
+ `Teams enabled: ${person.teamsEnabled === null ? "unknown" : String(person.teamsEnabled)}`,
241
+ ].join("\n")}\n`;
242
+ }
243
+ export function personImageOutput(image, base64, stdoutIsTTY) {
244
+ if (!base64 && stdoutIsTTY) {
245
+ throw new Error("Refusing to write a raw profile image to an interactive terminal. Pipe it to a file or use --base64.");
246
+ }
247
+ return base64 ? Buffer.from(`${image.data.toString("base64")}\n`, "utf8") : image.data;
248
+ }
249
+ function writeData(value, human, json) {
250
+ process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : human);
251
+ }
252
+ function parsePageSize(raw) {
253
+ const value = Number(raw);
254
+ if (!Number.isSafeInteger(value) || value < 1 || value > 200) {
255
+ throw new Error("--page-size must be an integer from 1 to 200");
256
+ }
257
+ return value;
258
+ }
259
+ function collect(value, previous) {
260
+ return [...previous, value];
261
+ }
262
+ function policyDecisionSummary(resolved) {
263
+ const active = resolved.policies.filter(({ policy }) => policy.active).length;
264
+ if (active === 0)
265
+ return "Allowed: no active policy applies.\n";
266
+ return `Allowed by ${active} active polic${active === 1 ? "y" : "ies"}.\n`;
267
+ }
268
+ export function selectedTarget(options) {
269
+ if ((options.chat ? 1 : 0) + (options.channel ? 1 : 0) !== 1) {
270
+ throw new Error("Exactly one of --chat or --channel is required");
271
+ }
272
+ return options.chat
273
+ ? { kind: "chat", id: options.chat }
274
+ : { kind: "channel", id: options.channel };
275
+ }
276
+ async function messageBody(body) {
277
+ let value = body;
278
+ if (value === undefined) {
279
+ if (process.stdin.isTTY)
280
+ throw new Error("Provide --body or pipe a message on stdin");
281
+ const chunks = [];
282
+ for await (const chunk of process.stdin)
283
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
284
+ value = Buffer.concat(chunks).toString("utf8");
285
+ }
286
+ if (!value.trim())
287
+ throw new Error("Message body must not be empty");
288
+ return value;
289
+ }
290
+ async function runWithStatus(program, json, status, operation) {
291
+ configureDiagnostics({ progress: Boolean(process.stderr.isTTY) && !json, debug: program.opts().debug === true });
292
+ showStatus(status);
293
+ try {
294
+ return await operation();
295
+ }
296
+ finally {
297
+ clearStatus();
298
+ }
299
+ }
300
+ export function createProgram(options = {}) {
301
+ const paths = storagePaths(options.storageRoot);
302
+ const subjectPath = options.subjectPath;
303
+ const program = new Command()
304
+ .name("teams-cli")
305
+ .description("A safety-conscious command-line client for persistent Microsoft Teams sessions")
306
+ .version(CLI_VERSION)
307
+ .option("--debug", "Show sanitized HTTP request diagnostics")
308
+ .option("--profile <name>", "Named configuration profile")
309
+ .option("--tenant <tenant-id>", "Microsoft tenant ID")
310
+ .option("--user <user-id>", "Microsoft user object ID")
311
+ .addOption(new Option("--browser <browser>", "Browser used for Microsoft sign-in").choices(["edge", "chrome"]))
312
+ .showHelpAfterError();
313
+ registerVersionCommand(program);
314
+ registerSkillsCommand(program, options.storageRoot);
315
+ const auth = program.command("auth").description("Manage Microsoft Teams authentication");
316
+ auth
317
+ .command("login")
318
+ .description("Sign in with Microsoft and save the Teams session")
319
+ .option("--username <login-name>", "Microsoft login name used by automated login")
320
+ .option("--password-command <absolute-path>", "Executable that writes the password to stdout")
321
+ .option("--headless", "Run automated login without a visible browser")
322
+ .action(async (loginOptions) => {
323
+ const context = await runtimeContext(program, paths);
324
+ const policies = await resolvePolicies(paths, subjectPath);
325
+ const reportPolicyWarnings = policyWarningReporter();
326
+ reportPolicyWarnings(policyStatusWarnings(policies));
327
+ if (context.tenantId && context.userId) {
328
+ reportPolicyWarnings(requirePolicyIdentity(policies, {
329
+ tenantId: context.tenantId,
330
+ userId: context.userId,
331
+ }));
332
+ }
333
+ const selectedUsername = loginOptions.username ?? context.username;
334
+ process.stderr.write(`Opening ${context.browser === "edge" ? "Microsoft Edge" : "Google Chrome"} for Teams sign-in…\n`);
335
+ const session = await runWithStatus(program, false, "Signing in…", () => login(paths, {
336
+ browser: context.browser,
337
+ ...(context.tenantId ? { tenant: context.tenantId } : {}),
338
+ ...(context.userId ? { user: context.userId } : {}),
339
+ ...(selectedUsername ? { username: selectedUsername } : {}),
340
+ ...(loginOptions.passwordCommand ? { passwordCommand: loginOptions.passwordCommand } : {}),
341
+ ...(loginOptions.headless ? { headless: true } : {}),
342
+ authorizeIdentity: async (identity) => {
343
+ reportPolicyWarnings(requirePolicyIdentity(policies, identity));
344
+ },
345
+ }));
346
+ await saveProfile(paths, context.profileName, {
347
+ tenantId: session.tenantId,
348
+ userId: session.userId,
349
+ ...(session.username ? { username: session.username } : selectedUsername ? { username: selectedUsername } : {}),
350
+ browser: context.browser,
351
+ });
352
+ process.stdout.write(`Logged in to tenant ${session.tenantId}.\n`);
353
+ });
354
+ auth
355
+ .command("refresh")
356
+ .description("Refresh all tokens or one token")
357
+ .addArgument(new Argument("[token]", "Token to refresh")
358
+ .choices(["all", "access", "skype", "chat", "search"])
359
+ .default("all"))
360
+ .action(async (target) => {
361
+ const runtime = await authorizedRuntime(program, paths, subjectPath);
362
+ const result = await runWithStatus(program, false, `Refreshing ${target} token${target === "all" ? "s" : ""}…`, () => refreshTokens(paths, runtime.identity, target, runtime.context.browser));
363
+ process.stdout.write(renderRefreshResult(result));
364
+ });
365
+ auth
366
+ .command("whoami")
367
+ .description("Validate the saved session and show its user and token expiry")
368
+ .action(async () => {
369
+ const runtime = await authorizedRuntime(program, paths, subjectPath);
370
+ const session = await runWithStatus(program, false, "Validating session…", () => validateSession(paths, runtime.identity, runtime.context.browser));
371
+ outputWhoami(describeSession(session));
372
+ });
373
+ auth
374
+ .command("tokens")
375
+ .alias("token")
376
+ .description("Show saved tokens or their decoded JWT claims")
377
+ .addArgument(new Argument("[token]", "Token to show")
378
+ .choices(["all", "access", "skype", "chat", "search"])
379
+ .default("all"))
380
+ .option("--decode", "Show only the decoded JWT claims")
381
+ .action(async (target, tokenOptions) => {
382
+ const runtime = await authorizedRuntime(program, paths, subjectPath);
383
+ if (!tokenOptions.decode) {
384
+ runtime.reportPolicyWarnings(requireRawTokenExport(runtime.policies, runtime.identity));
385
+ }
386
+ const session = requireCurrentSession(await loadSession(paths, runtime.identity));
387
+ process.stdout.write(renderTokens(session, target, tokenOptions.decode ?? false));
388
+ });
389
+ auth
390
+ .command("logout")
391
+ .description("Remove the saved session and dedicated browser profiles")
392
+ .action(async () => {
393
+ const runtime = await authorizedRuntime(program, paths, subjectPath);
394
+ await logout(paths, runtime.identity);
395
+ process.stdout.write("Logged out. Local Teams tokens and browser profiles were removed.\n");
396
+ });
397
+ const profile = program.command("profile").description("Manage named configuration profiles");
398
+ profile.command("list").description("List configured profiles").action(async () => {
399
+ const config = await loadProfiles(paths);
400
+ const names = Object.keys(config.profiles).sort();
401
+ process.stdout.write(names.length ? `${names.join("\n")}\n` : "No profiles configured.\n");
402
+ });
403
+ profile.command("show").description("Show one profile or the selected profile")
404
+ .argument("[name]", "Profile name")
405
+ .action(async (name) => {
406
+ const context = await runtimeContext(program, paths);
407
+ const profileName = name ?? context.profileName;
408
+ const config = await loadProfiles(paths);
409
+ const stored = config.profiles[profileName];
410
+ if (!stored)
411
+ throw new Error(`Profile ${profileName} does not exist`);
412
+ process.stdout.write(stringify({ name: profileName, ...stored }));
413
+ });
414
+ profile.command("save").description("Save the effective tenant, user, and browser as a profile")
415
+ .argument("<name>", "Profile name")
416
+ .action(async (name) => {
417
+ const context = await runtimeContext(program, paths);
418
+ const identity = requireRuntimeIdentity(context);
419
+ const session = requireCurrentSession(await loadSession(paths, identity));
420
+ await saveProfile(paths, name, {
421
+ ...identity,
422
+ ...(session.username ? { username: session.username } : {}),
423
+ browser: context.browser,
424
+ });
425
+ process.stdout.write(`Saved profile ${name}.\n`);
426
+ });
427
+ profile.command("remove").description("Remove profile configuration without deleting its session")
428
+ .argument("<name>", "Profile name")
429
+ .action(async (name) => {
430
+ if (!await removeProfile(paths, name))
431
+ throw new Error(`Profile ${name} does not exist`);
432
+ process.stdout.write(`Removed profile ${name}. Authentication was retained.\n`);
433
+ });
434
+ const policy = program.command("policy").description("Manage subject-based safety policies");
435
+ policy.command("init").description("Create a restrictive inactive policy")
436
+ .argument("<name>", "Policy name")
437
+ .option("--subject <absolute-path-glob>", "Subject path glob; repeat for multiple paths", collect, [])
438
+ .action(async (name, policyOptions) => {
439
+ const context = await runtimeContext(program, paths);
440
+ const record = await initializePolicy(paths, name, context, policyOptions.subject, subjectPath);
441
+ process.stdout.write(`Created inactive policy ${record.policy.name} at ${record.file}.\n`);
442
+ process.stderr.write("Warning: The policy is in audit mode and is not enforcing restrictions.\n");
443
+ });
444
+ policy.command("list").description("List all policies")
445
+ .action(async () => {
446
+ const records = await loadPolicyStore(paths);
447
+ if (records.length === 0) {
448
+ process.stdout.write("No policies configured.\n");
449
+ return;
450
+ }
451
+ for (const record of records) {
452
+ process.stdout.write(`${record.policy.name}\t${record.policy.active ? "active" : "inactive"}\t${record.file}\n`);
453
+ }
454
+ });
455
+ policy.command("show").description("Show one named policy or policies applying to a path")
456
+ .argument("[name]", "Policy name")
457
+ .option("--path <path>", "Concrete subject path to evaluate")
458
+ .action(async (name, policyOptions) => {
459
+ const reportWarnings = policyWarningReporter();
460
+ if (name) {
461
+ const record = await resolvePolicyByName(paths, name);
462
+ reportWarnings(record.permissionWarnings);
463
+ process.stdout.write(`# ${record.file}\n${stringify(record.policy)}`);
464
+ return;
465
+ }
466
+ const resolved = await resolvePolicies(paths, policyOptions.path ?? subjectPath);
467
+ reportWarnings(policyStatusWarnings(resolved));
468
+ if (resolved.policies.length === 0) {
469
+ process.stdout.write(`No policy applies to subject path ${resolved.subjectPath}.\n`);
470
+ return;
471
+ }
472
+ for (const [index, record] of resolved.policies.entries()) {
473
+ if (index > 0)
474
+ process.stdout.write("---\n");
475
+ process.stdout.write(`# ${record.file}\n${stringify(record.policy)}`);
476
+ }
477
+ });
478
+ const policyCheck = policy.command("check").description("Check an effective policy decision");
479
+ policyCheck.command("send").description("Check a chat or channel send")
480
+ .option("--chat <chat-id>", "Target chat ID")
481
+ .option("--channel <channel-id>", "Target channel ID")
482
+ .option("--path <path>", "Concrete subject path to evaluate")
483
+ .action(async (checkOptions) => {
484
+ const context = await runtimeContext(program, paths);
485
+ const identity = requireRuntimeIdentity(context);
486
+ const resolved = await resolvePolicies(paths, checkOptions.path ?? subjectPath);
487
+ const reportWarnings = policyWarningReporter();
488
+ reportWarnings(policyStatusWarnings(resolved));
489
+ reportWarnings(requireMessageSend(resolved, identity, selectedTarget(checkOptions)));
490
+ process.stdout.write(policyDecisionSummary(resolved));
491
+ });
492
+ policyCheck.command("raw-tokens").description("Check raw bearer-token export")
493
+ .option("--path <path>", "Concrete subject path to evaluate")
494
+ .action(async (checkOptions) => {
495
+ const context = await runtimeContext(program, paths);
496
+ const identity = requireRuntimeIdentity(context);
497
+ const resolved = await resolvePolicies(paths, checkOptions.path ?? subjectPath);
498
+ const reportWarnings = policyWarningReporter();
499
+ reportWarnings(policyStatusWarnings(resolved));
500
+ reportWarnings(requireRawTokenExport(resolved, identity));
501
+ process.stdout.write(policyDecisionSummary(resolved));
502
+ });
503
+ policy.command("activate").description("Activate one policy for enforcement")
504
+ .argument("<name>", "Policy name")
505
+ .action(async (name) => {
506
+ const activated = await activatePolicy(await resolvePolicyByName(paths, name));
507
+ process.stdout.write(`Activated policy ${activated.policy.name} at ${activated.file}.\n`);
508
+ const instruction = policyProtectionInstruction(activated.file);
509
+ if (instruction) {
510
+ process.stderr.write(`Recommended additional protection: ${instruction}\n`);
511
+ }
512
+ else {
513
+ process.stderr.write("Protect the active policy with an administrator-managed read-only ACL.\n");
514
+ }
515
+ });
516
+ const person = program.command("person").description("Search and inspect Microsoft Teams people");
517
+ person.command("search")
518
+ .description("Search for people by name or email")
519
+ .argument("<query>", "Person name or email query")
520
+ .option("--json", "Output stable JSON")
521
+ .action(async (query, options) => {
522
+ const result = await runWithStatus(program, options.json ?? false, "Searching for people…", () => withAuthorizedDataSession(program, paths, subjectPath, "search", (session) => searchPeople(session, query)));
523
+ writeData(result, renderPeople(result), options.json ?? false);
524
+ });
525
+ person.command("get")
526
+ .description("Get a detailed person profile by email, object ID, or MRI")
527
+ .argument("<email-or-id>", "Email address, object ID, or Teams MRI")
528
+ .option("--json", "Output stable JSON")
529
+ .action(async (identifier, options) => {
530
+ const result = await runWithStatus(program, options.json ?? false, "Loading person profile…", () => withAuthorizedDataSession(program, paths, subjectPath, "access", (session) => getPerson(session, identifier)));
531
+ writeData(result, renderPerson(result), options.json ?? false);
532
+ });
533
+ person.command("image")
534
+ .description("Stream a person's authenticated profile image")
535
+ .argument("<email-or-id>", "Email address, object ID, or Teams MRI")
536
+ .option("--base64", "Output one base64-encoded line instead of raw image bytes")
537
+ .addOption(new Option("--size <pixels>", "Requested image size; unavailable sizes fall back")
538
+ .choices(["48", "64", "96", "120", "240", "360", "432", "504", "648", "max"])
539
+ .default("max"))
540
+ .action(async (identifier, options) => {
541
+ const base64 = options.base64 ?? false;
542
+ if (!base64 && process.stdout.isTTY) {
543
+ personImageOutput({ data: Buffer.alloc(0), contentType: "application/octet-stream" }, false, true);
544
+ }
545
+ const result = await runWithStatus(program, base64, "Loading person image…", () => withAuthorizedDataSession(program, paths, subjectPath, "access", (session) => getPersonImage(session, identifier, options.size)));
546
+ process.stdout.write(personImageOutput(result, base64, Boolean(process.stdout.isTTY)));
547
+ });
548
+ const chat = program.command("chat").description("Read Microsoft Teams chats");
549
+ chat
550
+ .command("list")
551
+ .description("List the server-provided chat collection and participants")
552
+ .option("--cursor <cursor>", "Opaque cursor returned by the previous page")
553
+ .option("--json", "Output stable JSON")
554
+ .action(async (options) => {
555
+ const result = await runWithStatus(program, options.json ?? false, "Loading chats…", () => withAuthorizedDataSession(program, paths, subjectPath, ["chat", "skype"], (session) => listChats(session, options.cursor)));
556
+ writeData(result, renderChats(result), options.json ?? false);
557
+ });
558
+ chat
559
+ .command("get")
560
+ .description("Get one chat by ID")
561
+ .argument("<chat-id>", "Teams chat ID")
562
+ .option("--json", "Output stable JSON")
563
+ .action(async (chatId, options) => {
564
+ const result = await runWithStatus(program, options.json ?? false, "Loading chat…", () => withAuthorizedDataSession(program, paths, subjectPath, ["chat", "skype"], (session) => getChat(session, chatId)));
565
+ writeData(result, renderChatResult(result), options.json ?? false);
566
+ });
567
+ const channel = program.command("channel").description("Read Microsoft Teams channels");
568
+ channel.command("list").description("List channels across available teams")
569
+ .option("--json", "Output stable JSON")
570
+ .action(async (options) => {
571
+ const result = await runWithStatus(program, options.json ?? false, "Loading channels…", () => withAuthorizedDataSession(program, paths, subjectPath, ["chat", "skype"], (session) => listChannels(session)));
572
+ writeData(result, renderChannels(result), options.json ?? false);
573
+ });
574
+ channel.command("get").description("Get one channel by ID")
575
+ .argument("<channel-id>", "Teams channel ID")
576
+ .option("--json", "Output stable JSON")
577
+ .action(async (channelId, options) => {
578
+ const result = await runWithStatus(program, options.json ?? false, "Loading channel…", () => withAuthorizedDataSession(program, paths, subjectPath, ["chat", "skype"], (session) => getChannel(session, channelId)));
579
+ writeData(result, renderChannelResult(result), options.json ?? false);
580
+ });
581
+ const message = program.command("message").description("Read and send Microsoft Teams messages");
582
+ message.command("list").description("List a server-provided page of messages")
583
+ .option("--chat <chat-id>", "Target chat ID")
584
+ .option("--channel <channel-id>", "Target channel ID")
585
+ .option("--page-size <number>", "Server page size from 1 to 200", parsePageSize)
586
+ .option("--cursor <cursor>", "Opaque cursor returned by the previous page")
587
+ .option("--json", "Output stable JSON")
588
+ .action(async (options) => {
589
+ if (options.cursor && options.pageSize !== undefined) {
590
+ throw new Error("--cursor cannot be combined with --page-size");
591
+ }
592
+ const target = selectedTarget(options);
593
+ const result = await runWithStatus(program, options.json ?? false, "Fetching messages…", () => withAuthorizedDataSession(program, paths, subjectPath, "skype", (session) => listMessages(session, target, options)));
594
+ writeData(result, renderMessages(result), options.json ?? false);
595
+ });
596
+ message.command("get").description("Get one message by ID")
597
+ .argument("<message-id>", "Teams message ID")
598
+ .option("--chat <chat-id>", "Target chat ID")
599
+ .option("--channel <channel-id>", "Target channel ID")
600
+ .option("--json", "Output stable JSON")
601
+ .action(async (messageId, options) => {
602
+ const target = selectedTarget(options);
603
+ const result = await runWithStatus(program, options.json ?? false, "Fetching message…", () => withAuthorizedDataSession(program, paths, subjectPath, "skype", (session) => getMessage(session, target, messageId)));
604
+ writeData(result, renderMessageResult(result), options.json ?? false);
605
+ });
606
+ message.command("send").description("Send one policy-authorized plain-text message")
607
+ .option("--chat <chat-id>", "Target chat ID")
608
+ .option("--channel <channel-id>", "Target channel ID")
609
+ .option("--body <text>", "Plain-text message body; otherwise read stdin")
610
+ .option("--json", "Output stable JSON")
611
+ .action(async (options) => {
612
+ const target = selectedTarget(options);
613
+ const body = await messageBody(options.body);
614
+ const requestId = `${Date.now()}${randomInt(1_000_000).toString().padStart(6, "0")}`;
615
+ const sessionId = randomUUID();
616
+ const result = await runWithStatus(program, options.json ?? false, "Sending message…", async () => {
617
+ const runtime = await authorizedRuntime(program, paths, subjectPath);
618
+ runtime.reportPolicyWarnings(requireMessageSend(runtime.policies, runtime.identity, target));
619
+ return withDataSession(paths, runtime.identity, runtime.context.browser, "skype", (session) => sendMessage(session, target, body, requestId, sessionId, async () => {
620
+ const currentPolicies = await resolvePolicies(paths, subjectPath);
621
+ runtime.reportPolicyWarnings(policyStatusWarnings(currentPolicies));
622
+ runtime.reportPolicyWarnings(requireMessageSend(currentPolicies, runtime.identity, target));
623
+ }));
624
+ });
625
+ writeData(result, renderMessageSendResult(result), options.json ?? false);
626
+ });
627
+ return program;
628
+ }
629
+ const entrypoint = process.argv[1];
630
+ if (entrypoint && realpathSync(entrypoint) === realpathSync(fileURLToPath(import.meta.url))) {
631
+ const run = async () => {
632
+ if (process.argv[2] === "--internal-update-check" && process.env.TEAMS_CLI_UPDATE_WORKER === "1") {
633
+ const currentVersion = process.argv[3];
634
+ const file = process.argv[4];
635
+ if (currentVersion && file)
636
+ await runUpdateWorker(currentVersion, file);
637
+ return;
638
+ }
639
+ await prepareUpdateNotification({ currentVersion: CLI_VERSION });
640
+ await createProgram().parseAsync(process.argv);
641
+ };
642
+ run().catch((error) => {
643
+ const message = error instanceof Error ? error.message : String(error);
644
+ process.stderr.write(`${message}\n`);
645
+ process.exitCode = 1;
646
+ });
647
+ }