m365-agent-cli 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +916 -0
- package/package.json +50 -0
- package/src/cli.ts +100 -0
- package/src/commands/auto-reply.ts +182 -0
- package/src/commands/calendar.ts +576 -0
- package/src/commands/counter.ts +87 -0
- package/src/commands/create-event.ts +544 -0
- package/src/commands/delegates.ts +286 -0
- package/src/commands/delete-event.ts +321 -0
- package/src/commands/drafts.ts +502 -0
- package/src/commands/files.ts +532 -0
- package/src/commands/find.ts +195 -0
- package/src/commands/findtime.ts +270 -0
- package/src/commands/folders.ts +177 -0
- package/src/commands/forward-event.ts +49 -0
- package/src/commands/graph-calendar.ts +217 -0
- package/src/commands/login.ts +195 -0
- package/src/commands/mail.ts +950 -0
- package/src/commands/oof.ts +263 -0
- package/src/commands/outlook-categories.ts +173 -0
- package/src/commands/outlook-graph.ts +880 -0
- package/src/commands/planner.ts +1678 -0
- package/src/commands/respond.ts +291 -0
- package/src/commands/rooms.ts +210 -0
- package/src/commands/rules.ts +511 -0
- package/src/commands/schedule.ts +109 -0
- package/src/commands/send.ts +204 -0
- package/src/commands/serve.ts +14 -0
- package/src/commands/sharepoint.ts +179 -0
- package/src/commands/site-pages.ts +163 -0
- package/src/commands/subscribe.ts +103 -0
- package/src/commands/subscriptions.ts +29 -0
- package/src/commands/suggest.ts +155 -0
- package/src/commands/todo.ts +2092 -0
- package/src/commands/update-event.ts +608 -0
- package/src/commands/update.ts +88 -0
- package/src/commands/verify-token.ts +62 -0
- package/src/commands/whoami.ts +74 -0
- package/src/index.ts +190 -0
- package/src/lib/atomic-write.ts +20 -0
- package/src/lib/attach-link-spec.test.ts +24 -0
- package/src/lib/attach-link-spec.ts +70 -0
- package/src/lib/attachments.ts +79 -0
- package/src/lib/auth.ts +192 -0
- package/src/lib/calendar-range.test.ts +41 -0
- package/src/lib/calendar-range.ts +103 -0
- package/src/lib/dates.test.ts +74 -0
- package/src/lib/dates.ts +137 -0
- package/src/lib/delegate-client.test.ts +74 -0
- package/src/lib/delegate-client.ts +322 -0
- package/src/lib/ews-client.ts +3418 -0
- package/src/lib/git-commit.ts +4 -0
- package/src/lib/glitchtip-eligibility.ts +220 -0
- package/src/lib/glitchtip.ts +253 -0
- package/src/lib/global-env.ts +3 -0
- package/src/lib/graph-auth.ts +223 -0
- package/src/lib/graph-calendar-client.test.ts +118 -0
- package/src/lib/graph-calendar-client.ts +112 -0
- package/src/lib/graph-client.test.ts +107 -0
- package/src/lib/graph-client.ts +1058 -0
- package/src/lib/graph-constants.ts +12 -0
- package/src/lib/graph-directory.ts +116 -0
- package/src/lib/graph-event.ts +134 -0
- package/src/lib/graph-schedule.ts +173 -0
- package/src/lib/graph-subscriptions.ts +94 -0
- package/src/lib/graph-user-path.ts +13 -0
- package/src/lib/jwt-utils.ts +34 -0
- package/src/lib/markdown.test.ts +21 -0
- package/src/lib/markdown.ts +174 -0
- package/src/lib/mime-type.ts +106 -0
- package/src/lib/oof-client.test.ts +59 -0
- package/src/lib/oof-client.ts +122 -0
- package/src/lib/outlook-graph-client.test.ts +146 -0
- package/src/lib/outlook-graph-client.ts +649 -0
- package/src/lib/outlook-master-categories.ts +145 -0
- package/src/lib/package-info.ts +59 -0
- package/src/lib/places-client.ts +144 -0
- package/src/lib/planner-client.ts +1226 -0
- package/src/lib/rules-client.ts +178 -0
- package/src/lib/sharepoint-client.ts +101 -0
- package/src/lib/site-pages-client.ts +73 -0
- package/src/lib/todo-client.test.ts +298 -0
- package/src/lib/todo-client.ts +1309 -0
- package/src/lib/url-validation.ts +40 -0
- package/src/lib/utils.ts +45 -0
- package/src/lib/webhook-server.ts +51 -0
- package/src/test/auth.test.ts +104 -0
- package/src/test/cli.integration.test.ts +1083 -0
- package/src/test/ews-client.test.ts +268 -0
- package/src/test/mocks/index.ts +375 -0
- package/src/test/mocks/responses.ts +861 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate GlitchTip: only when this install matches the latest npm release and the embedded
|
|
3
|
+
* commit matches the GitHub tag v{version} (see docs/GLITCHTIP.md).
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { atomicWriteUtf8File } from './atomic-write.js';
|
|
10
|
+
import { COMMIT_SHA } from './git-commit.js';
|
|
11
|
+
import { getPackageJsonPath, getPackageVersion } from './package-info.js';
|
|
12
|
+
|
|
13
|
+
const NPM_PKG = 'm365-agent-cli';
|
|
14
|
+
/** How long to cache npm + GitHub tag lookups (avoid hammering registries on every invocation). */
|
|
15
|
+
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
16
|
+
const FETCH_TIMEOUT_MS = 12_000;
|
|
17
|
+
|
|
18
|
+
export interface GlitchTipEligibility {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface EligibilityCache {
|
|
24
|
+
fetchedAt: number;
|
|
25
|
+
npmLatest: string;
|
|
26
|
+
/** Resolved commit SHA for tag v{npmLatest} */
|
|
27
|
+
tagCommitSha: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseGithubRepo(repoUrl: string | undefined): { owner: string; repo: string } | null {
|
|
31
|
+
if (!repoUrl) return null;
|
|
32
|
+
const m = repoUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/i);
|
|
33
|
+
if (!m) return null;
|
|
34
|
+
return { owner: m[1], repo: m[2] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readGithubCoords(): Promise<{ owner: string; repo: string } | null> {
|
|
38
|
+
const raw = readFileSync(getPackageJsonPath(), 'utf8');
|
|
39
|
+
const j = JSON.parse(raw) as { repository?: { url?: string } };
|
|
40
|
+
return parseGithubRepo(j.repository?.url);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cacheFile(): string {
|
|
44
|
+
return join(homedir(), '.config', 'm365-agent-cli', 'cache', 'glitchtip-eligibility.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function assertEligibilityCache(data: unknown): EligibilityCache {
|
|
48
|
+
if (!data || typeof data !== 'object') throw new Error('invalid eligibility cache');
|
|
49
|
+
const o = data as Record<string, unknown>;
|
|
50
|
+
if (typeof o.fetchedAt !== 'number' || !Number.isFinite(o.fetchedAt)) throw new Error('invalid eligibility cache');
|
|
51
|
+
if (typeof o.npmLatest !== 'string' || o.npmLatest.trim().length < 3 || o.npmLatest.length > 80) {
|
|
52
|
+
throw new Error('invalid eligibility cache');
|
|
53
|
+
}
|
|
54
|
+
const sha = o.tagCommitSha;
|
|
55
|
+
if (sha !== null && (typeof sha !== 'string' || !/^[0-9a-f]{40}$/i.test(sha))) {
|
|
56
|
+
throw new Error('invalid eligibility cache');
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
fetchedAt: o.fetchedAt,
|
|
60
|
+
npmLatest: o.npmLatest.trim(),
|
|
61
|
+
tagCommitSha: sha
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function loadCache(): Promise<EligibilityCache | null> {
|
|
66
|
+
try {
|
|
67
|
+
const raw = await readFile(cacheFile(), 'utf8');
|
|
68
|
+
return assertEligibilityCache(JSON.parse(raw));
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function saveCache(c: EligibilityCache): Promise<void> {
|
|
75
|
+
const safe = assertEligibilityCache(c);
|
|
76
|
+
await atomicWriteUtf8File(cacheFile(), JSON.stringify(safe, null, 2), 0o600);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assertEligibilityFetchUrl(url: string): URL {
|
|
80
|
+
const u = new URL(url);
|
|
81
|
+
if (u.protocol !== 'https:') throw new Error('unsupported URL');
|
|
82
|
+
const host = u.hostname;
|
|
83
|
+
if (host !== 'registry.npmjs.org' && host !== 'api.github.com') {
|
|
84
|
+
throw new Error('unsupported URL');
|
|
85
|
+
}
|
|
86
|
+
return u;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function fetchJson<T>(url: string): Promise<{ ok: boolean; data?: T; status: number }> {
|
|
90
|
+
let href: string;
|
|
91
|
+
try {
|
|
92
|
+
href = assertEligibilityFetchUrl(url).toString();
|
|
93
|
+
} catch {
|
|
94
|
+
return { ok: false, status: 0 };
|
|
95
|
+
}
|
|
96
|
+
const ac = new AbortController();
|
|
97
|
+
const t = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
|
|
98
|
+
try {
|
|
99
|
+
const r = await fetch(href, {
|
|
100
|
+
signal: ac.signal,
|
|
101
|
+
headers: {
|
|
102
|
+
Accept: 'application/vnd.github+json',
|
|
103
|
+
'User-Agent': 'm365-agent-cli-glitchtip-eligibility'
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (!r.ok) return { ok: false, status: r.status };
|
|
107
|
+
const data = (await r.json()) as T;
|
|
108
|
+
return { ok: true, data, status: r.status };
|
|
109
|
+
} catch {
|
|
110
|
+
return { ok: false, status: 0 };
|
|
111
|
+
} finally {
|
|
112
|
+
clearTimeout(t);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function fetchNpmLatestVersion(): Promise<string | null> {
|
|
117
|
+
const url = `https://registry.npmjs.org/${NPM_PKG}/latest`;
|
|
118
|
+
const r = await fetchJson<{ version?: string }>(url);
|
|
119
|
+
if (!r.ok || !r.data?.version) return null;
|
|
120
|
+
return r.data.version.trim();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Resolve annotated or lightweight tag to commit SHA. */
|
|
124
|
+
async function resolveTagCommitSha(owner: string, repo: string, version: string): Promise<string | null> {
|
|
125
|
+
const refUrl = `https://api.github.com/repos/${owner}/${repo}/git/ref/tags/v${encodeURIComponent(version)}`;
|
|
126
|
+
const ref = await fetchJson<{ object?: { sha?: string; type?: string } }>(refUrl);
|
|
127
|
+
if (!ref.ok || !ref.data?.object?.sha) return null;
|
|
128
|
+
const { sha, type } = ref.data.object;
|
|
129
|
+
if (type === 'commit') return sha;
|
|
130
|
+
if (type === 'tag') {
|
|
131
|
+
const tagUrl = `https://api.github.com/repos/${owner}/${repo}/git/tags/${sha}`;
|
|
132
|
+
const tag = await fetchJson<{ object?: { sha?: string; type?: string } }>(tagUrl);
|
|
133
|
+
if (!tag.ok || !tag.data?.object?.sha) return null;
|
|
134
|
+
return tag.data.object.sha;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeSha(s: string): string {
|
|
140
|
+
return s.trim().toLowerCase();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Returns whether GlitchTip should initialize. Uses cached npm + GitHub tag resolution (~1h).
|
|
145
|
+
*/
|
|
146
|
+
export async function checkGlitchTipEligibility(): Promise<GlitchTipEligibility> {
|
|
147
|
+
if (process.env.GLITCHTIP_SKIP_VERSION_CHECK === '1' || process.env.GLITCHTIP_SKIP_VERSION_CHECK === 'true') {
|
|
148
|
+
return { ok: true, reason: 'GLITCHTIP_SKIP_VERSION_CHECK' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const currentVersion = await getPackageVersion();
|
|
152
|
+
const coords = await readGithubCoords();
|
|
153
|
+
if (!coords) {
|
|
154
|
+
return { ok: false, reason: 'package.json repository URL could not be parsed for GitHub' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let cache = await loadCache();
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
let npmLatest: string;
|
|
160
|
+
let tagCommitSha: string | null;
|
|
161
|
+
|
|
162
|
+
if (cache && now - cache.fetchedAt < CACHE_TTL_MS && cache.npmLatest) {
|
|
163
|
+
npmLatest = cache.npmLatest;
|
|
164
|
+
tagCommitSha = cache.tagCommitSha;
|
|
165
|
+
} else {
|
|
166
|
+
const nv = await fetchNpmLatestVersion();
|
|
167
|
+
if (!nv) {
|
|
168
|
+
return { ok: false, reason: 'could not fetch latest version from npm registry' };
|
|
169
|
+
}
|
|
170
|
+
npmLatest = nv;
|
|
171
|
+
tagCommitSha = await resolveTagCommitSha(coords.owner, coords.repo, npmLatest);
|
|
172
|
+
cache = { fetchedAt: now, npmLatest, tagCommitSha };
|
|
173
|
+
try {
|
|
174
|
+
await saveCache(cache);
|
|
175
|
+
} catch {
|
|
176
|
+
// cache is optional
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (currentVersion !== npmLatest) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
reason: `not on latest npm release (running ${currentVersion}, latest is ${npmLatest})`
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const localSha = normalizeSha(COMMIT_SHA);
|
|
188
|
+
if (localSha === 'unknown') {
|
|
189
|
+
if (
|
|
190
|
+
process.env.GLITCHTIP_ALLOW_UNVERIFIED_COMMIT === '1' ||
|
|
191
|
+
process.env.GLITCHTIP_ALLOW_UNVERIFIED_COMMIT === 'true'
|
|
192
|
+
) {
|
|
193
|
+
return {
|
|
194
|
+
ok: true,
|
|
195
|
+
reason: 'COMMIT_SHA unknown; allowed by GLITCHTIP_ALLOW_UNVERIFIED_COMMIT (git match skipped)'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
reason:
|
|
201
|
+
'COMMIT_SHA is unknown — run `npm run embed-sha` from a git checkout before release, or set GLITCHTIP_ALLOW_UNVERIFIED_COMMIT=1'
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!tagCommitSha) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
reason: `could not resolve GitHub tag v${npmLatest} (create tag on the release commit)`
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (normalizeSha(tagCommitSha) !== localSha) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
reason: `embedded commit does not match GitHub tag v${npmLatest} (local ${localSha.slice(0, 7)}… vs tag ${normalizeSha(tagCommitSha).slice(0, 7)}…)`
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { ok: true, reason: `npm ${npmLatest} and commit match tag v${npmLatest}` };
|
|
220
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional error reporting to [GlitchTip](https://glitchtip.com/) (Sentry-compatible ingest).
|
|
3
|
+
* Set `GLITCHTIP_DSN` or `SENTRY_DSN` to enable. No reporting when unset.
|
|
4
|
+
*
|
|
5
|
+
* Events are scrubbed to avoid PII: no argv content, no user/request/breadcrumbs, paths and
|
|
6
|
+
* messages redacted (see stripSensitiveEventData).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ErrorEvent, EventHint, StackFrame } from '@sentry/core';
|
|
10
|
+
import { captureException, flush, init, isInitialized } from '@sentry/node';
|
|
11
|
+
import { checkGlitchTipEligibility } from './glitchtip-eligibility.js';
|
|
12
|
+
import { getPackageVersionSync } from './package-info.js';
|
|
13
|
+
|
|
14
|
+
/** Keys we never attach to reports (callers may pass `extra`; these are dropped). */
|
|
15
|
+
const EXTRA_KEY_DENYLIST =
|
|
16
|
+
/^(body|bodyHtml|html|text|message|subject|snippet|preview|email|mail|address|token|password|secret|authorization|refresh|credential|content|attachment)/i;
|
|
17
|
+
|
|
18
|
+
const EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
|
|
19
|
+
/** Looks like a JWT or opaque bearer token (redact). */
|
|
20
|
+
const JWT_LIKE_RE = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/gi;
|
|
21
|
+
|
|
22
|
+
function redactString(s: string): string {
|
|
23
|
+
return s
|
|
24
|
+
.replace(EMAIL_RE, '[email]')
|
|
25
|
+
.replace(JWT_LIKE_RE, '[token]')
|
|
26
|
+
.replace(/Bearer\s+[A-Za-z0-9._~-]+/gi, 'Bearer [token]')
|
|
27
|
+
.replace(/\\Users\\[^\\]+\\/gi, '\\Users\\<user>\\')
|
|
28
|
+
.replace(/\/Users\/[^/]+/g, '/Users/<user>')
|
|
29
|
+
.replace(/\/home\/[^/]+/g, '/home/<user>');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function deepRedactValue(value: unknown, depth = 0): unknown {
|
|
33
|
+
if (depth > 6) return '[truncated]';
|
|
34
|
+
if (typeof value === 'string') return redactString(value);
|
|
35
|
+
if (value === null || typeof value !== 'object') return value;
|
|
36
|
+
if (Array.isArray(value)) return value.slice(0, 50).map((v) => deepRedactValue(v, depth + 1));
|
|
37
|
+
const out: Record<string, unknown> = {};
|
|
38
|
+
for (const [k, v] of Object.entries(value)) {
|
|
39
|
+
if (EXTRA_KEY_DENYLIST.test(k)) continue;
|
|
40
|
+
out[k] = deepRedactValue(v, depth + 1);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sanitizeExtraForReport(extra: Record<string, unknown>): Record<string, unknown> {
|
|
46
|
+
const cleaned: Record<string, unknown> = {};
|
|
47
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
48
|
+
if (EXTRA_KEY_DENYLIST.test(k)) continue;
|
|
49
|
+
cleaned[k] = deepRedactValue(v);
|
|
50
|
+
}
|
|
51
|
+
return cleaned;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Remove PII from the outbound event; keeps stack structure for debugging. */
|
|
55
|
+
function stripSensitiveEventData(event: ErrorEvent): void {
|
|
56
|
+
delete event.user;
|
|
57
|
+
delete event.request;
|
|
58
|
+
delete event.server_name;
|
|
59
|
+
event.breadcrumbs = [];
|
|
60
|
+
|
|
61
|
+
if (event.sdkProcessingMetadata) {
|
|
62
|
+
delete event.sdkProcessingMetadata;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (event.message) {
|
|
66
|
+
event.message = redactString(event.message);
|
|
67
|
+
}
|
|
68
|
+
if (event.logentry?.message) {
|
|
69
|
+
event.logentry.message = redactString(event.logentry.message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (event.extra) {
|
|
73
|
+
const next: Record<string, unknown> = {};
|
|
74
|
+
for (const [k, v] of Object.entries(event.extra)) {
|
|
75
|
+
if (EXTRA_KEY_DENYLIST.test(k)) continue;
|
|
76
|
+
next[k] = deepRedactValue(v);
|
|
77
|
+
}
|
|
78
|
+
event.extra = next;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (event.tags) {
|
|
82
|
+
const next: Record<string, string | number | boolean> = {};
|
|
83
|
+
for (const [k, v] of Object.entries(event.tags)) {
|
|
84
|
+
if (typeof v === 'string') next[k] = redactString(v);
|
|
85
|
+
else if (typeof v === 'number' || typeof v === 'boolean') next[k] = v;
|
|
86
|
+
}
|
|
87
|
+
event.tags = next;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const exValues = event.exception?.values;
|
|
91
|
+
if (exValues) {
|
|
92
|
+
for (const ex of exValues) {
|
|
93
|
+
if (ex.value) ex.value = redactString(ex.value);
|
|
94
|
+
scrubFrames(ex.stacktrace?.frames);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (event.modules) {
|
|
99
|
+
const next: Record<string, string> = {};
|
|
100
|
+
for (const [k, v] of Object.entries(event.modules)) {
|
|
101
|
+
next[k] = typeof v === 'string' ? redactString(v) : String(v);
|
|
102
|
+
}
|
|
103
|
+
event.modules = next;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (event.contexts) {
|
|
107
|
+
const { os, runtime, app } = event.contexts;
|
|
108
|
+
event.contexts = {};
|
|
109
|
+
if (os && typeof os === 'object') {
|
|
110
|
+
const o = os as Record<string, unknown>;
|
|
111
|
+
event.contexts.os = {
|
|
112
|
+
name: typeof o.name === 'string' ? o.name : undefined,
|
|
113
|
+
version: typeof o.version === 'string' ? o.version : undefined,
|
|
114
|
+
kernel_version: typeof o.kernel_version === 'string' ? o.kernel_version : undefined
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (runtime && typeof runtime === 'object') {
|
|
118
|
+
event.contexts.runtime = runtime;
|
|
119
|
+
}
|
|
120
|
+
if (app && typeof app === 'object') {
|
|
121
|
+
const a = app as Record<string, unknown>;
|
|
122
|
+
event.contexts.app = {
|
|
123
|
+
app_start_time: typeof a.app_start_time === 'string' ? a.app_start_time : undefined,
|
|
124
|
+
app_memory: typeof a.app_memory === 'number' ? a.app_memory : undefined
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (event.threads?.values) {
|
|
130
|
+
for (const thread of event.threads.values) {
|
|
131
|
+
delete thread.name;
|
|
132
|
+
scrubFrames(thread.stacktrace?.frames);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function scrubFrames(frames: StackFrame[] | undefined): void {
|
|
138
|
+
if (!frames) return;
|
|
139
|
+
for (const frame of frames) {
|
|
140
|
+
if (frame.filename) frame.filename = redactString(frame.filename);
|
|
141
|
+
if (frame.abs_path) frame.abs_path = redactString(frame.abs_path);
|
|
142
|
+
if (frame.context_line) frame.context_line = redactString(frame.context_line);
|
|
143
|
+
if (frame.pre_context) frame.pre_context = frame.pre_context.map(redactString);
|
|
144
|
+
if (frame.post_context) frame.post_context = frame.post_context.map(redactString);
|
|
145
|
+
delete frame.vars;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getDsn(): string | undefined {
|
|
150
|
+
const raw = process.env.GLITCHTIP_DSN?.trim() || process.env.SENTRY_DSN?.trim();
|
|
151
|
+
if (!raw) return undefined;
|
|
152
|
+
const disabled = process.env.GLITCHTIP_ENABLED === '0' || process.env.GLITCHTIP_ENABLED === 'false';
|
|
153
|
+
if (disabled) return undefined;
|
|
154
|
+
return raw;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Errno codes that usually indicate environment/network, not application bugs. */
|
|
158
|
+
const DEFAULT_IGNORE_ERRNO = new Set([
|
|
159
|
+
'ECONNREFUSED',
|
|
160
|
+
'ECONNRESET',
|
|
161
|
+
'ETIMEDOUT',
|
|
162
|
+
'ENOTFOUND',
|
|
163
|
+
'EAI_AGAIN',
|
|
164
|
+
'ENETUNREACH'
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
function beforeSend(event: ErrorEvent, hint: EventHint): ErrorEvent | null {
|
|
168
|
+
const reportAll = process.env.GLITCHTIP_REPORT_ALL === '1';
|
|
169
|
+
if (!reportAll) {
|
|
170
|
+
const ex = hint.originalException;
|
|
171
|
+
if (ex && typeof ex === 'object' && 'code' in ex) {
|
|
172
|
+
const code = String((ex as NodeJS.ErrnoException).code);
|
|
173
|
+
if (code && DEFAULT_IGNORE_ERRNO.has(code)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (ex instanceof Error) {
|
|
179
|
+
const msg = ex.message;
|
|
180
|
+
if (/invalid[_ ]?grant|refresh[_ ]?token.*invalid|AADSTS\d+/i.test(msg)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
stripSensitiveEventData(event);
|
|
187
|
+
enrichSafeEvent(event);
|
|
188
|
+
return event;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Safe CLI context only: no argv text (may contain addresses, paths, mail snippets). */
|
|
192
|
+
function enrichSafeEvent(event: ErrorEvent): void {
|
|
193
|
+
const argv = process.argv.slice(2);
|
|
194
|
+
const first = argv[0];
|
|
195
|
+
event.tags = {
|
|
196
|
+
...(event.tags ?? {}),
|
|
197
|
+
'cli.argc': String(argv.length)
|
|
198
|
+
};
|
|
199
|
+
if (first && /^[a-z][a-z0-9-]*$/i.test(first) && first.length <= 48) {
|
|
200
|
+
event.tags['cli.command'] = first;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let didInit = false;
|
|
205
|
+
|
|
206
|
+
/** Call once after `loadGlobalEnv()` so `.env` can define `GLITCHTIP_DSN`. */
|
|
207
|
+
export async function initGlitchTip(): Promise<void> {
|
|
208
|
+
if (didInit) return;
|
|
209
|
+
didInit = true;
|
|
210
|
+
|
|
211
|
+
const dsn = getDsn();
|
|
212
|
+
if (!dsn) return;
|
|
213
|
+
|
|
214
|
+
const eligibility = await checkGlitchTipEligibility();
|
|
215
|
+
if (!eligibility.ok) {
|
|
216
|
+
if (process.env.GLITCHTIP_DEBUG_ELIGIBILITY === '1' || process.env.GLITCHTIP_DEBUG_ELIGIBILITY === 'true') {
|
|
217
|
+
console.error(`[GlitchTip] Disabled: ${eligibility.reason ?? 'not eligible'}`);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const releaseFromEnv = process.env.GLITCHTIP_RELEASE?.trim();
|
|
223
|
+
const release =
|
|
224
|
+
releaseFromEnv && releaseFromEnv.length > 0 ? releaseFromEnv : `m365-agent-cli@${getPackageVersionSync()}`;
|
|
225
|
+
|
|
226
|
+
init({
|
|
227
|
+
dsn,
|
|
228
|
+
sendDefaultPii: false,
|
|
229
|
+
environment: process.env.GLITCHTIP_ENVIRONMENT || process.env.NODE_ENV || 'production',
|
|
230
|
+
release,
|
|
231
|
+
tracesSampleRate: 0,
|
|
232
|
+
profilesSampleRate: 0,
|
|
233
|
+
maxBreadcrumbs: 0,
|
|
234
|
+
beforeBreadcrumb: () => null,
|
|
235
|
+
beforeSend: beforeSend as (event: ErrorEvent, hint: EventHint) => ErrorEvent | null
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Wait for the transport to finish (call before `process.exit` after a capture). */
|
|
240
|
+
export async function flushGlitchTip(timeoutMs = 2000): Promise<boolean> {
|
|
241
|
+
if (!isInitialized()) return true;
|
|
242
|
+
return flush(timeoutMs);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Report an exception (e.g. Commander parse failure). No-op if GlitchTip is not configured. */
|
|
246
|
+
export function captureCliException(error: unknown, extra?: Record<string, unknown>): void {
|
|
247
|
+
if (!isInitialized()) return;
|
|
248
|
+
if (extra && Object.keys(extra).length > 0) {
|
|
249
|
+
captureException(error, { extra: sanitizeExtraForReport(extra) });
|
|
250
|
+
} else {
|
|
251
|
+
captureException(error);
|
|
252
|
+
}
|
|
253
|
+
}
|