@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/update.js ADDED
@@ -0,0 +1,137 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { spawn } from "node:child_process";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { PACKAGE_NAME } from "./version.js";
8
+ export const UPDATE_INTERVAL_MS = 60 * 60 * 1000;
9
+ const UPDATE_TIMEOUT_MS = 3_000;
10
+ export function updateStateFile(storageRoot = join(homedir(), ".teams-cli")) {
11
+ return join(storageRoot, "update-check.json");
12
+ }
13
+ export function updateChecksDisabled(environment = process.env) {
14
+ const enabled = (value) => value === "1" || value?.toLowerCase() === "true";
15
+ return enabled(environment.NO_UPDATE_NOTIFIER) || enabled(environment.TEAMS_CLI_DISABLE_UPDATE_CHECK) ||
16
+ enabled(environment.CI) || enabled(environment.TEAMS_CLI_UPDATE_WORKER);
17
+ }
18
+ export async function loadUpdateState(file = updateStateFile()) {
19
+ try {
20
+ const parsed = JSON.parse(await readFile(file, "utf8"));
21
+ if (!parsed || typeof parsed !== "object")
22
+ return null;
23
+ const candidate = parsed;
24
+ if (candidate.version !== 1 || typeof candidate.checkedAt !== "string")
25
+ return null;
26
+ if (candidate.latestVersion !== undefined && typeof candidate.latestVersion !== "string")
27
+ return null;
28
+ if (candidate.pendingVersion !== undefined && typeof candidate.pendingVersion !== "string")
29
+ return null;
30
+ return candidate;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ async function saveUpdateState(state, file) {
37
+ await mkdir(dirname(file), { recursive: true, mode: 0o700 });
38
+ await chmod(dirname(file), 0o700);
39
+ const temporary = `${file}.${randomUUID()}.tmp`;
40
+ await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
41
+ await rename(temporary, file);
42
+ await chmod(file, 0o600);
43
+ }
44
+ export function parseVersion(value) {
45
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value);
46
+ if (!match)
47
+ return null;
48
+ return {
49
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
50
+ prerelease: match[4] ?? null,
51
+ };
52
+ }
53
+ export function isNewerVersion(current, candidate) {
54
+ const left = parseVersion(current);
55
+ const right = parseVersion(candidate);
56
+ if (!left || !right)
57
+ return false;
58
+ if (right.prerelease && !left.prerelease)
59
+ return false;
60
+ for (let index = 0; index < left.core.length; index += 1) {
61
+ const currentPart = left.core[index] ?? 0;
62
+ const candidatePart = right.core[index] ?? 0;
63
+ if (candidatePart !== currentPart)
64
+ return candidatePart > currentPart;
65
+ }
66
+ if (left.prerelease && !right.prerelease)
67
+ return true;
68
+ return Boolean(left.prerelease && right.prerelease && right.prerelease !== left.prerelease &&
69
+ right.prerelease.localeCompare(left.prerelease, undefined, { numeric: true }) > 0);
70
+ }
71
+ export async function runUpdateWorker(currentVersion, file = updateStateFile(), fetcher = fetch, now = new Date()) {
72
+ const previous = await loadUpdateState(file);
73
+ const next = {
74
+ version: 1,
75
+ checkedAt: now.toISOString(),
76
+ ...(previous?.latestVersion ? { latestVersion: previous.latestVersion } : {}),
77
+ ...(previous?.pendingVersion ? { pendingVersion: previous.pendingVersion } : {}),
78
+ };
79
+ try {
80
+ const encodedName = PACKAGE_NAME.replace("/", "%2F");
81
+ const response = await fetcher(`https://registry.npmjs.org/${encodedName}/latest`, {
82
+ headers: { accept: "application/json" },
83
+ signal: AbortSignal.timeout(UPDATE_TIMEOUT_MS),
84
+ });
85
+ if (!response.ok)
86
+ throw new Error(`npm registry returned ${response.status}`);
87
+ const payload = await response.json();
88
+ const latest = payload && typeof payload === "object" ? payload.version : undefined;
89
+ if (typeof latest !== "string")
90
+ throw new Error("npm registry returned no version");
91
+ next.latestVersion = latest;
92
+ if (isNewerVersion(currentVersion, latest))
93
+ next.pendingVersion = latest;
94
+ else
95
+ delete next.pendingVersion;
96
+ }
97
+ catch {
98
+ // Update checks are advisory and must never make a CLI command fail.
99
+ }
100
+ await saveUpdateState(next, file);
101
+ }
102
+ export async function prepareUpdateNotification(options) {
103
+ const environment = options.environment ?? process.env;
104
+ if (updateChecksDisabled(environment))
105
+ return;
106
+ const file = options.stateFile ?? updateStateFile();
107
+ const state = await loadUpdateState(file);
108
+ if (state?.pendingVersion && isNewerVersion(options.currentVersion, state.pendingVersion)) {
109
+ (options.stderr ?? process.stderr).write(`A new teams-cli version is available: ${options.currentVersion} → ${state.pendingVersion}. ` +
110
+ "Run `teams-cli version --upgrade`.\n");
111
+ const consumed = { ...state };
112
+ delete consumed.pendingVersion;
113
+ try {
114
+ await saveUpdateState(consumed, file);
115
+ }
116
+ catch {
117
+ // A read-only or damaged cache must not prevent the requested CLI command.
118
+ }
119
+ }
120
+ const now = options.now ?? new Date();
121
+ const checkedAt = state ? Date.parse(state.checkedAt) : Number.NaN;
122
+ if (Number.isFinite(checkedAt) && now.getTime() - checkedAt < UPDATE_INTERVAL_MS)
123
+ return;
124
+ if (options.spawnWorker) {
125
+ options.spawnWorker(file);
126
+ return;
127
+ }
128
+ const entrypoint = fileURLToPath(new URL("./cli.js", import.meta.url));
129
+ const child = spawn(process.execPath, [entrypoint, "--internal-update-check", options.currentVersion, file], {
130
+ detached: true,
131
+ stdio: "ignore",
132
+ windowsHide: true,
133
+ env: { ...environment, TEAMS_CLI_UPDATE_WORKER: "1" },
134
+ });
135
+ child.on("error", () => undefined);
136
+ child.unref();
137
+ }
@@ -0,0 +1,45 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join, win32 } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { PACKAGE_NAME } from "./version.js";
6
+ const execFileAsync = promisify(execFile);
7
+ export function npmInvocation(platform = process.platform, nodeExecutable = process.execPath, npmExecPath = process.env.npm_execpath) {
8
+ if (npmExecPath)
9
+ return { command: nodeExecutable, args: [npmExecPath] };
10
+ if (platform === "win32") {
11
+ return {
12
+ command: nodeExecutable,
13
+ args: [win32.join(win32.dirname(nodeExecutable), "node_modules", "npm", "bin", "npm-cli.js")],
14
+ };
15
+ }
16
+ return { command: "npm", args: [] };
17
+ }
18
+ const defaultRunner = (command, args) => new Promise((resolve, reject) => {
19
+ const child = spawn(command, [...args], { stdio: "inherit", shell: false, windowsHide: true });
20
+ child.once("error", reject);
21
+ child.once("exit", (code) => resolve(code ?? 1));
22
+ });
23
+ export async function upgradeCli(options = {}) {
24
+ const runner = options.runner ?? defaultRunner;
25
+ const npm = npmInvocation();
26
+ const installStatus = await runner(npm.command, [...npm.args, "install", "--global", `${PACKAGE_NAME}@latest`]);
27
+ if (installStatus !== 0) {
28
+ throw new Error(`npm upgrade failed with exit code ${installStatus}`);
29
+ }
30
+ const globalRoot = options.globalRoot ?? (async () => {
31
+ const { stdout } = await execFileAsync(npm.command, [...npm.args, "root", "--global"], {
32
+ encoding: "utf8",
33
+ windowsHide: true,
34
+ });
35
+ return stdout.trim();
36
+ });
37
+ const installedCli = join(await globalRoot(), ...PACKAGE_NAME.split("/"), "dist", "cli.js");
38
+ if (!existsSync(installedCli) && !options.globalRoot) {
39
+ throw new Error(`The updated CLI was not found at ${installedCli}`);
40
+ }
41
+ const reinstallStatus = await runner(process.execPath, [installedCli, "skills", "reinstall"]);
42
+ if (reinstallStatus !== 0) {
43
+ throw new Error(`The CLI was upgraded, but skill reinstallation failed with exit code ${reinstallStatus}`);
44
+ }
45
+ }
@@ -0,0 +1,7 @@
1
+ import { createRequire } from "node:module";
2
+ const metadata = createRequire(import.meta.url)("../package.json");
3
+ if (typeof metadata.name !== "string" || typeof metadata.version !== "string") {
4
+ throw new Error("Installed package metadata is invalid");
5
+ }
6
+ export const PACKAGE_NAME = metadata.name;
7
+ export const CLI_VERSION = metadata.version;
package/dist/yaml.js ADDED
@@ -0,0 +1,33 @@
1
+ import { parseDocument } from "yaml";
2
+ export function parseStrictYaml(raw, label) {
3
+ const document = parseDocument(raw, {
4
+ version: "1.2",
5
+ uniqueKeys: true,
6
+ merge: false,
7
+ prettyErrors: true,
8
+ });
9
+ if (document.errors.length > 0) {
10
+ throw new Error(`${label} is invalid YAML: ${document.errors[0]?.message ?? "unknown error"}`);
11
+ }
12
+ if (document.warnings.length > 0) {
13
+ throw new Error(`${label} uses unsupported YAML: ${document.warnings[0]?.message ?? "unknown warning"}`);
14
+ }
15
+ try {
16
+ return document.toJS({ maxAliasCount: 0 });
17
+ }
18
+ catch (error) {
19
+ const detail = error instanceof Error ? error.message : String(error);
20
+ throw new Error(`${label} uses unsupported YAML: ${detail}`);
21
+ }
22
+ }
23
+ export function requireObject(value, label) {
24
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
25
+ throw new Error(`${label} must be an object`);
26
+ }
27
+ return value;
28
+ }
29
+ export function rejectUnknownKeys(object, allowed, label) {
30
+ const unknown = Object.keys(object).find((key) => !allowed.includes(key));
31
+ if (unknown)
32
+ throw new Error(`${label} contains unknown field ${unknown}`);
33
+ }
@@ -0,0 +1,48 @@
1
+ # Releasing
2
+
3
+ Releases are published from GitHub Releases by `.github/workflows/publish-npm.yml`. The release tag must exactly equal `v` followed by the version in `package.json`.
4
+
5
+ ## Repository setup
6
+
7
+ Before the first release:
8
+
9
+ 1. Make the repository public so npm provenance can reference it.
10
+ 2. Protect `main` and require the CI and CodeQL checks.
11
+ 3. Enable secret scanning and push protection.
12
+ 4. Create a GitHub environment named `npm`, restrict it to release tags, and add a required reviewer.
13
+ 5. Enable two-factor authentication on the npm maintainer account.
14
+
15
+ ## First publication
16
+
17
+ npm cannot configure a trusted publisher until the package exists. Bootstrap `0.1.0` once:
18
+
19
+ 1. Create a short-lived granular npm token that can create the public package in the `@michaelschnyder` scope and can bypass publish 2FA for this one automated run.
20
+ 2. Add it to the protected `npm` environment as `NPM_BOOTSTRAP_TOKEN`.
21
+ 3. Confirm `npm run check`, `npm test`, `npm run build`, `npm run package:check`, and `npm run package:smoke` pass on a clean checkout.
22
+ 4. Create and publish the GitHub Release `v0.1.0`.
23
+ 5. Confirm the workflow and npm provenance succeed.
24
+
25
+ Immediately afterward, delete the GitHub secret and revoke the npm token.
26
+
27
+ ## Configure trusted publishing
28
+
29
+ On npm, configure the package's trusted publisher with:
30
+
31
+ - Provider: GitHub Actions
32
+ - Organization or user: `michaelschnyder`
33
+ - Repository: `teams-cli`
34
+ - Workflow: `publish-npm.yml`
35
+ - Environment: `npm`
36
+ - Allowed action: publish
37
+
38
+ Set package publishing access to require 2FA and disallow tokens. Later workflow runs authenticate with short-lived OIDC credentials and generate provenance automatically.
39
+
40
+ ## Subsequent releases
41
+
42
+ 1. Update `package.json` and `package-lock.json` to the same semantic version.
43
+ 2. Update this changelog and packaged skill metadata.
44
+ 3. Merge the release changes through protected `main`.
45
+ 4. Create and publish a GitHub Release with the exact matching `vX.Y.Z` tag.
46
+ 5. Verify the workflow, npm package contents, provenance, executable version, and `latest` distribution tag.
47
+
48
+ Published npm versions are immutable. If a release is defective, fix it in a new version rather than attempting to reuse or overwrite the published version.
@@ -0,0 +1,25 @@
1
+ # Authentication
2
+
3
+ Login uses a dedicated Edge or Chrome profile and stores the resulting Teams tokens under the verified tenant and user identity.
4
+
5
+ ```bash
6
+ teams-cli --tenant TENANT_ID auth login
7
+ teams-cli --profile test-alice auth login
8
+ teams-cli --profile personal auth whoami
9
+ teams-cli --profile personal auth refresh
10
+ teams-cli --profile personal auth logout
11
+ ```
12
+
13
+ `--user USER_ID` constrains login to the expected Microsoft object ID. The CLI rejects tokens for another tenant or user. The stable session identity is the pair `(tenantId, userId)`; usernames are mutable login hints.
14
+
15
+ Tokens for different users never share a session file. Browser state is also isolated by tenant, user, and browser. Selecting another browser does not invalidate existing tokens, but that browser needs its own authenticated state when browser-backed token acquisition is required.
16
+
17
+ Raw bearer tokens remain available:
18
+
19
+ ```bash
20
+ teams-cli --profile personal auth tokens
21
+ teams-cli --profile personal auth token access
22
+ teams-cli --profile personal auth tokens --decode
23
+ ```
24
+
25
+ If an active subject policy applies, raw bearer output requires `rawTokenExport: true` in every applicable active policy. Decoded JWT claims remain available. Treat exported tokens like passwords; another HTTP client can use them to bypass cooperative CLI policy checks.
@@ -0,0 +1,136 @@
1
+ # Policies
2
+
3
+ Policies are optional named YAML files under `~/.teams-cli/policies/`. They limit the effective identity, message destinations, and raw-token export for matching subject paths.
4
+
5
+ ## How policy selection works
6
+
7
+ The current subject is the canonical absolute path from which the CLI is invoked. This path selector is a convention of this CLI, not an external standard or a strong security identity. Symlinks are resolved before matching.
8
+
9
+ Each policy contains one or more absolute path patterns. Patterns use Node.js glob syntax:
10
+
11
+ ```yaml
12
+ subject:
13
+ paths:
14
+ - /Users/me/Workspaces/project
15
+ - /Users/me/Workspaces/client-*/**
16
+ ```
17
+
18
+ The paths within one policy are alternatives: matching any one makes that policy applicable. Several policies may apply to the same subject. Every active policy must allow an operation, so adding another active policy can only preserve or narrow access.
19
+
20
+ The CLI validates every `.yaml` file in the policy directory before matching. An unreadable, malformed, misnamed, unsupported, or dangerously writable active policy puts authenticated operations into fail-safe mode across every subject. Files without a `.yaml` extension are not policies and are ignored.
21
+
22
+ ## Inactive and active policies
23
+
24
+ New policies are inactive. An inactive policy runs in audit mode:
25
+
26
+ - The CLI warns on stderr that the policy is not enforcing restrictions.
27
+ - The policy is still evaluated.
28
+ - The CLI warns when it would deny the selected identity or operation.
29
+ - The operation remains allowed unless another applicable active policy denies it.
30
+
31
+ An active policy enforces its identity and allowlists. Active policies cannot be deactivated through the CLI. This makes activation deliberate without claiming that the file itself is locked.
32
+
33
+ ## Create and refine a policy
34
+
35
+ Create a named restrictive policy for the current path. Without explicit subjects, initialization adds both the current canonical path and a descendant glob so commands from its subdirectories remain covered:
36
+
37
+ ```bash
38
+ teams-cli --profile personal policy init project-agent
39
+ ```
40
+
41
+ Supply several subject patterns by repeating `--subject`:
42
+
43
+ ```bash
44
+ teams-cli --profile personal policy init client-projects \
45
+ --subject '/Users/me/Workspaces/client-a/**' \
46
+ --subject '/Users/me/Workspaces/client-b/**'
47
+ ```
48
+
49
+ Subject patterns must be absolute. Quote patterns so the shell does not expand them before the CLI receives them.
50
+
51
+ The generated policy is restrictive and inactive:
52
+
53
+ ```yaml
54
+ version: 1
55
+ name: project-agent
56
+ active: false
57
+ subject:
58
+ paths:
59
+ - /Users/me/Workspaces/project
60
+ - /Users/me/Workspaces/project/**
61
+ identity:
62
+ tenantId: tenant-id
63
+ userId: user-id
64
+ allow:
65
+ messageSend:
66
+ chats: []
67
+ channels: []
68
+ rawTokenExport: false
69
+ ```
70
+
71
+ Edit the file outside the CLI and add only the exact, case-sensitive chat and channel IDs required by the subject. A chat entry never permits a channel with the same text. Set `rawTokenExport: true` only when complete bearer tokens are genuinely required; decoded claims do not need that permission.
72
+
73
+ Inspect configured or applicable policies:
74
+
75
+ ```bash
76
+ teams-cli policy list
77
+ teams-cli policy show project-agent
78
+ teams-cli policy show
79
+ teams-cli policy show --path /absolute/path/to/check
80
+ ```
81
+
82
+ Check representative decisions while the policy is still inactive. Warnings show what audit mode would deny:
83
+
84
+ ```bash
85
+ teams-cli --profile personal policy check send --chat CHAT_ID
86
+ teams-cli --profile personal policy check send --channel CHANNEL_ID
87
+ teams-cli --profile personal policy check raw-tokens
88
+ ```
89
+
90
+ ## Activate and protect a policy
91
+
92
+ Activate enforcement by name:
93
+
94
+ ```bash
95
+ teams-cli policy activate project-agent
96
+ ```
97
+
98
+ On POSIX systems, the command prints an exact additional protection instruction such as:
99
+
100
+ ```bash
101
+ chmod 400 -- '/Users/me/.teams-cli/policies/project-agent.yaml'
102
+ ```
103
+
104
+ Activation and filesystem protection are separate:
105
+
106
+ - `active: true` makes the CLI enforce the policy.
107
+ - Owner-read-only permissions reduce accidental same-user modification.
108
+ - An active owner-writable policy is enforced but produces a warning.
109
+ - An active policy or policy directory writable by group or other users fails closed.
110
+ - On Windows, use an administrator-managed read-only ACL instead.
111
+
112
+ Read-only permissions are defense in depth, not an immutable lock. A process with sufficient owner or administrator privileges can still replace the file or use exported tokens outside this CLI.
113
+
114
+ ## Deactivate, revise, or remove a policy
115
+
116
+ The CLI intentionally has no deactivate, edit, or remove command. Use `policy show NAME` to obtain the exact file path. If the store is malformed, the error identifies the offending file.
117
+
118
+ On POSIX systems, make that one file writable before changing it:
119
+
120
+ ```bash
121
+ chmod u+w '/exact/policy/path.yaml'
122
+ ```
123
+
124
+ To return to audit mode, set `active: false`. To revise an active policy, deactivate it first, make the changes, use `policy check`, and activate it again. To remove it completely, delete only that exact file:
125
+
126
+ ```bash
127
+ rm '/exact/policy/path.yaml'
128
+ ```
129
+
130
+ Removing the last applicable active policy may make the subject unrestricted. Confirm the result with `policy show` and `policy check`.
131
+
132
+ If permissions are controlled by a read-only mount, sandbox, separate OS identity, or administrator ACL, revise the policy at that external enforcement layer.
133
+
134
+ ## Security boundary
135
+
136
+ Policies primarily prevent accidental messages to the wrong destinations. Strong, non-bypassable enforcement requires controls outside the agent process, such as a separate OS identity, read-only container mount, restricted network egress, or server-side Teams permissions.
@@ -0,0 +1,57 @@
1
+ # Profiles
2
+
3
+ Profiles are named configuration baselines similar to AWS CLI profiles. They do not own tokens and are not security boundaries.
4
+
5
+ Profiles are stored in `~/.teams-cli/config.yaml`. For example:
6
+
7
+ ```yaml
8
+ version: 1
9
+ profiles:
10
+ default:
11
+ tenantId: personal-tenant-id
12
+ userId: personal-user-id
13
+ username: me@example.test
14
+ browser: edge
15
+ test-alice:
16
+ tenantId: test-tenant-id
17
+ userId: alice-user-id
18
+ username: alice@example.test
19
+ browser: chrome
20
+ ```
21
+
22
+ When neither `--profile` nor `TEAMS_CLI_PROFILE` is provided, the CLI selects the profile named `default` if it exists:
23
+
24
+ ```bash
25
+ teams-cli auth whoami
26
+ teams-cli message list --chat CHAT_ID
27
+ ```
28
+
29
+ Select another profile explicitly without changing the default:
30
+
31
+ ```bash
32
+ teams-cli --profile test-alice auth whoami
33
+ ```
34
+
35
+ ```bash
36
+ teams-cli profile list
37
+ teams-cli profile show personal
38
+ teams-cli --tenant TENANT_ID --user USER_ID --browser chrome profile save personal
39
+ teams-cli profile remove personal
40
+ ```
41
+
42
+ Login creates or updates the selected profile with the verified tenant, user, username, and browser:
43
+
44
+ ```bash
45
+ teams-cli --profile test-alice --tenant TENANT_ID auth login
46
+ ```
47
+
48
+ Selection and precedence are:
49
+
50
+ 1. Global command options.
51
+ 2. `TEAMS_CLI_PROFILE`, `TEAMS_CLI_TENANT`, `TEAMS_CLI_USER`, and `TEAMS_CLI_BROWSER`.
52
+ 3. The selected profile, or the profile named `default` when none is selected.
53
+ 4. Built-in defaults such as Edge.
54
+
55
+ The `default` profile is a selection fallback, not a source of field-by-field inheritance. A named profile does not inherit missing tenant, user, username, or browser fields from `default`; this prevents fields from two identities being combined accidentally. Several profiles may refer to the same tenant/user session. Removing a profile does not remove tokens; use `auth logout` with the corresponding identity for that.
56
+
57
+ Policies are evaluated after all profile, environment, and flag overrides. Flags can override profile values but cannot weaken an applicable active policy.
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@michaelschnyder/teams-cli",
3
+ "version": "0.1.0",
4
+ "description": "A safety-conscious CLI for persistent Microsoft Teams sessions",
5
+ "type": "module",
6
+ "bin": {
7
+ "teams-cli": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "docs/use",
12
+ "docs/releasing.md",
13
+ "README.md",
14
+ "SECURITY.md",
15
+ "CHANGELOG.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "dev": "tsx src/cli.ts",
20
+ "clean": "node scripts/clean.mjs",
21
+ "build": "npm run clean && tsc && node scripts/copy-skills.mjs && node scripts/sync-skill-versions.mjs && node scripts/make-executable.mjs",
22
+ "check": "tsc --noEmit",
23
+ "test": "node --import tsx --test test/**/*.test.ts",
24
+ "test:e2e": "node --import tsx --test test/e2e/*.e2e.ts",
25
+ "test:live": "npm run test:e2e",
26
+ "prepack": "npm run build",
27
+ "package:check": "node scripts/check-package.mjs",
28
+ "package:smoke": "node scripts/smoke-package.mjs"
29
+ },
30
+ "keywords": [
31
+ "microsoft-teams",
32
+ "teams",
33
+ "cli",
34
+ "automation",
35
+ "agent-skills"
36
+ ],
37
+ "author": "Michael Schnyder",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/michaelschnyder/teams-cli.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/michaelschnyder/teams-cli/issues"
45
+ },
46
+ "homepage": "https://github.com/michaelschnyder/teams-cli#readme",
47
+ "publishConfig": {
48
+ "access": "public",
49
+ "provenance": true
50
+ },
51
+ "dependencies": {
52
+ "commander": "^14.0.3",
53
+ "playwright-core": "^1.54.2",
54
+ "yaml": "^2.9.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^24.2.0",
58
+ "tsx": "^4.20.3",
59
+ "typescript": "^5.9.2"
60
+ },
61
+ "engines": {
62
+ "node": ">=22.20.0"
63
+ }
64
+ }