ask-marcel-office-cli 1.5.0 → 1.5.2
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/CHANGELOG.md +36 -5
- package/dist/cli.js +110 -43
- package/dist/commands.json +6 -7
- package/dist/index.js +109 -42
- package/dist/infra/auth.d.ts +2 -1
- package/docs/COMMANDS.md +1 -1
- package/docs/commands.json +6 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `ask-marcel-office-cli` are documented here.
|
|
4
4
|
|
|
5
|
+
## 1.5.2
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`find-chats-with-user` now surfaces cross-tenant 1:1 chats.** An externally-homed
|
|
10
|
+
counterpart comes back from the Teams chat roster as a bare object-id — no name, no
|
|
11
|
+
email — so a name search could never match it, and a real, active 1:1 returned a
|
|
12
|
+
silent `matchCount: 0`. The command now hydrates every bare **direct (1:1)** chat via
|
|
13
|
+
`/chats/{id}/members` and re-runs the match, so a cross-tenant counterpart is found
|
|
14
|
+
even when they were already resolved in a meeting under a different identity. When
|
|
15
|
+
nothing matches but bare members remain, it returns a `hint` + `unresolvedMemberCount`
|
|
16
|
+
rather than a confidently-empty result.
|
|
17
|
+
- **`list-chat-members` reads on the basic Teams token** (`ChatMember.Read`) instead of
|
|
18
|
+
the login-only elevated (M365ChatClient) token, so it no longer fails with "Elevated
|
|
19
|
+
token expired" on the command path. `next-page` routes `/chats/{id}/members` cursors on
|
|
20
|
+
the basic token to match. Chat _metadata_ (`list-chats` / `get-chat`) still requires
|
|
21
|
+
the elevated token.
|
|
22
|
+
|
|
23
|
+
## 1.5.1
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **No browser window opens per command once a secondary token lapses** (completes
|
|
28
|
+
the 1.5.0 auth fix). The elevated / Teams-chat (chatsvcagg / ic3) token recaptures
|
|
29
|
+
each launched a _visible_ browser that "opens and closes within seconds" to
|
|
30
|
+
silently re-capture — per command, per process, with no cross-process throttle —
|
|
31
|
+
so after the short-lived elevated token (~59 min) expired, every elevated or
|
|
32
|
+
Teams-chat command popped a window. The command-path auth now **fails fast** with
|
|
33
|
+
an actionable "run `ask-marcel login`" instead; browser capture is reserved for
|
|
34
|
+
the explicit `login` command, which re-captures all four tokens in one session.
|
|
35
|
+
|
|
5
36
|
## 1.5.0
|
|
6
37
|
|
|
7
38
|
Agent-ergonomics, a faster cold-start, a new command, and an LLM-safety pass.
|
|
@@ -50,11 +81,11 @@ change to note**: byte commands now refuse to inline a payload over ~1 MB withou
|
|
|
50
81
|
|
|
51
82
|
### Fixed
|
|
52
83
|
|
|
53
|
-
- **Auth no longer pops a browser per command on re-auth.**
|
|
54
|
-
|
|
55
|
-
process, with no cross-process throttle, so a batch/agent run stacked up
|
|
56
|
-
|
|
57
|
-
|
|
84
|
+
- **Auth no longer pops a browser per command on re-auth.** The primary-token
|
|
85
|
+
refresh-fallback used to launch the Chrome extension-capture window — once per
|
|
86
|
+
process, with no cross-process throttle, so a batch/agent run stacked up windows.
|
|
87
|
+
Command re-auth now uses a headed Edge sign-in only when the silent refresh
|
|
88
|
+
genuinely fails; the extension capture is reserved for explicit `login --use-extension`.
|
|
58
89
|
- **Stale doc numbers** — the `help-json` size hints (terse-category ~16 → ~6 KB
|
|
59
90
|
after the trim) and the README command list now match reality.
|
|
60
91
|
|
package/dist/cli.js
CHANGED
|
@@ -2457,7 +2457,7 @@ import updateNotifier from "update-notifier";
|
|
|
2457
2457
|
// package.json
|
|
2458
2458
|
var package_default = {
|
|
2459
2459
|
name: "ask-marcel-office-cli",
|
|
2460
|
-
version: "1.5.
|
|
2460
|
+
version: "1.5.2",
|
|
2461
2461
|
description: "Microsoft Graph CLI + library — typed Bun/TypeScript wrapper around 150+ Graph operations (read + on-the-fly PDF/markdown conversion + federated Microsoft Search) reachable from a Teams browser-OAuth token.",
|
|
2462
2462
|
license: "MIT",
|
|
2463
2463
|
author: "Vincent Delacourt <vincent.delacourt@adama-development.com>",
|
|
@@ -3475,7 +3475,8 @@ var SCOPES = "https://graph.microsoft.com/.default openid profile offline_access
|
|
|
3475
3475
|
var SPA_ORIGIN = "https://teams.microsoft.com";
|
|
3476
3476
|
var TEAMS_URL2 = "https://teams.microsoft.com/";
|
|
3477
3477
|
var DEFAULT_CHATSVCAGG_REGION2 = "emea";
|
|
3478
|
-
var
|
|
3478
|
+
var failFastSecondaryMessage = (token, commands) => `${token} token is expired or was not captured at login. Run \`ask-marcel login\` to (re)capture it — the CLI does not open a browser per command for this token. (Commands that need it: ${commands}.)`;
|
|
3479
|
+
var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logger, fs, systemBrowserAuthFn, usePlaywrightFallback = true, skipSystemBrowser = false, recaptureSecondaryViaBrowser = true) => {
|
|
3479
3480
|
const systemBrowserAuth = systemBrowserAuthFn ?? defaultSystemBrowserAuth(logger, skipSystemBrowser);
|
|
3480
3481
|
const readCache = async () => {
|
|
3481
3482
|
const r = await fs.readJson(cachePath);
|
|
@@ -3657,12 +3658,12 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
3657
3658
|
};
|
|
3658
3659
|
const recoverableElevatedFailureMessage = (reason) => {
|
|
3659
3660
|
if (reason === "launch_timeout") {
|
|
3660
|
-
return "elevated browser launch timed out (15s) — likely a corrupt persistent profile or filesystem lock. Run `ask-marcel logout && ask-marcel login` to wipe the profile and retry. (Commands that need this token: list-chats, get-chat,
|
|
3661
|
+
return "elevated browser launch timed out (15s) — likely a corrupt persistent profile or filesystem lock. Run `ask-marcel logout && ask-marcel login` to wipe the profile and retry. (Commands that need this token: list-chats, get-chat, the historical-version download / convert commands.)";
|
|
3661
3662
|
}
|
|
3662
3663
|
if (reason === "navigation_failed") {
|
|
3663
|
-
return "elevated capture failed: navigation to m365.cloud.microsoft did not complete — network issue, corp-proxy block, or tenant policy. Check connectivity and retry. If persistent, the elevated commands (list-chats / get-chat /
|
|
3664
|
+
return "elevated capture failed: navigation to m365.cloud.microsoft did not complete — network issue, corp-proxy block, or tenant policy. Check connectivity and retry. If persistent, the elevated commands (list-chats / get-chat / historical-version downloads) will be unavailable.";
|
|
3664
3665
|
}
|
|
3665
|
-
return "elevated token capture timed out — silent SSO against m365.cloud.microsoft did not yield a Bearer within 20s. The persistent browser-profile cookies are likely expired. Run `ask-marcel logout && ask-marcel login` — this now wipes the profile too. (Commands that need this token: list-chats, get-chat,
|
|
3666
|
+
return "elevated token capture timed out — silent SSO against m365.cloud.microsoft did not yield a Bearer within 20s. The persistent browser-profile cookies are likely expired. Run `ask-marcel logout && ask-marcel login` — this now wipes the profile too. (Commands that need this token: list-chats, get-chat, the historical-version download / convert commands.)";
|
|
3666
3667
|
};
|
|
3667
3668
|
const recaptureElevated = async () => {
|
|
3668
3669
|
try {
|
|
@@ -3697,6 +3698,8 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
3697
3698
|
logger.info("auth.elevated.cache_hit");
|
|
3698
3699
|
return ok(validated.value);
|
|
3699
3700
|
}
|
|
3701
|
+
if (!recaptureSecondaryViaBrowser)
|
|
3702
|
+
return err({ type: "auth_failed", message: failFastSecondaryMessage("Elevated (M365)", "list-chats, get-chat, download-drive-item-version") });
|
|
3700
3703
|
return recaptureElevatedShared();
|
|
3701
3704
|
};
|
|
3702
3705
|
const freshChatsvcaggToken = (cached) => {
|
|
@@ -3747,6 +3750,11 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
3747
3750
|
logger.info("auth.chatsvcagg.cache_hit");
|
|
3748
3751
|
return ok(accessTokenUnsafe(fresh));
|
|
3749
3752
|
}
|
|
3753
|
+
if (!recaptureSecondaryViaBrowser)
|
|
3754
|
+
return err({
|
|
3755
|
+
type: "auth_failed",
|
|
3756
|
+
message: failFastSecondaryMessage("chatsvcagg (Teams chat)", "list-teams-chats-with-messages, list-teams-chat-messages, get-teams-chat-message, find-chats-with-user")
|
|
3757
|
+
});
|
|
3750
3758
|
return recaptureChatsvcaggShared();
|
|
3751
3759
|
};
|
|
3752
3760
|
const getChatsvcaggRegion = async () => {
|
|
@@ -3802,6 +3810,8 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
3802
3810
|
logger.info("auth.ic3.cache_hit");
|
|
3803
3811
|
return ok(accessTokenUnsafe(fresh));
|
|
3804
3812
|
}
|
|
3813
|
+
if (!recaptureSecondaryViaBrowser)
|
|
3814
|
+
return err({ type: "auth_failed", message: failFastSecondaryMessage("ic3 (Teams chat history)", "list-teams-chat-history") });
|
|
3805
3815
|
return recaptureIc3Shared();
|
|
3806
3816
|
};
|
|
3807
3817
|
const logout = async () => {
|
|
@@ -3859,7 +3869,7 @@ var createAuthManager = (deps) => {
|
|
|
3859
3869
|
freshCachedToken: createFreshCachedTokenProbe(fs, deps.cachePath),
|
|
3860
3870
|
onProgress: stderrProgress
|
|
3861
3871
|
});
|
|
3862
|
-
return createAuthManagerFromApi(browserAuth, deps.cachePath, browserProfileDir, deps.logger, fs, deps.systemBrowserAuth, deps.usePlaywrightFallback, deps.skipSystemBrowser);
|
|
3872
|
+
return createAuthManagerFromApi(browserAuth, deps.cachePath, browserProfileDir, deps.logger, fs, deps.systemBrowserAuth, deps.usePlaywrightFallback, deps.skipSystemBrowser, deps.recaptureSecondaryViaBrowser);
|
|
3863
3873
|
};
|
|
3864
3874
|
// src/infra/network-error.ts
|
|
3865
3875
|
var REQUEST_TIMEOUT_MS = 60000;
|
|
@@ -4338,7 +4348,7 @@ var buildDeps = (config = {}) => {
|
|
|
4338
4348
|
const processRunner = config.processRunner ?? defaultProcessRunner();
|
|
4339
4349
|
const logger = createWinstonLogger({ logLevel });
|
|
4340
4350
|
const makeAuth = config.createAuth ?? createAuthManager;
|
|
4341
|
-
const auth = makeAuth({ cachePath, logger, fs, skipSystemBrowser: true, usePlaywrightFallback: true });
|
|
4351
|
+
const auth = makeAuth({ cachePath, logger, fs, skipSystemBrowser: true, usePlaywrightFallback: true, recaptureSecondaryViaBrowser: false });
|
|
4342
4352
|
const graph = createGraphClient(auth);
|
|
4343
4353
|
const makeLoginAuth = ({ useExtension }) => makeAuth({ cachePath, logger, fs, skipSystemBrowser: !useExtension, usePlaywrightFallback: !useExtension });
|
|
4344
4354
|
return { logger, auth, graph, processRunner, fs, makeLoginAuth };
|
|
@@ -23004,7 +23014,7 @@ __export(exports_list_chat_members, {
|
|
|
23004
23014
|
});
|
|
23005
23015
|
var baseSchema21 = exports_external.object({ chatId: exports_external.string().min(1) });
|
|
23006
23016
|
var CHAT_MEMBERS_ODATA_KEYS = ["skip", "select", "filter"];
|
|
23007
|
-
var inner5 =
|
|
23017
|
+
var inner5 = buildPickODataListCommand((p) => `/chats/${p.chatId}/members`, baseSchema21, CHAT_MEMBERS_ODATA_KEYS);
|
|
23008
23018
|
var execute42 = async (graph, params) => {
|
|
23009
23019
|
const result = await inner5.execute(graph, params);
|
|
23010
23020
|
if (result.ok)
|
|
@@ -23039,8 +23049,7 @@ var meta44 = {
|
|
|
23039
23049
|
],
|
|
23040
23050
|
example: "ask-marcel list-chat-members --chat-id '19:abc...@thread.v2'",
|
|
23041
23051
|
responseShape: "collection of Microsoft Graph `conversationMember` resources under `value[]`",
|
|
23042
|
-
pagination: true
|
|
23043
|
-
needsElevatedToken: true
|
|
23052
|
+
pagination: true
|
|
23044
23053
|
};
|
|
23045
23054
|
|
|
23046
23055
|
// src/use-cases/commands/list-drive-item-permissions.ts
|
|
@@ -24002,8 +24011,13 @@ __export(exports_next_page, {
|
|
|
24002
24011
|
execute: () => execute74
|
|
24003
24012
|
});
|
|
24004
24013
|
var PREFIX = "https://graph.microsoft.com/v1.0";
|
|
24005
|
-
var
|
|
24006
|
-
|
|
24014
|
+
var requiresElevated = (path) => {
|
|
24015
|
+
if (path.startsWith("/me/chats"))
|
|
24016
|
+
return true;
|
|
24017
|
+
if (path.startsWith("/chats/"))
|
|
24018
|
+
return !path.includes("/members");
|
|
24019
|
+
return false;
|
|
24020
|
+
};
|
|
24007
24021
|
var schema74 = exports_external.object({
|
|
24008
24022
|
url: exports_external.string().min(1).refine((v) => v.startsWith(`${PREFIX}/`), { message: `must be a Microsoft Graph v1.0 URL starting with ${PREFIX}/` })
|
|
24009
24023
|
});
|
|
@@ -26621,15 +26635,43 @@ var projectMember = (m) => {
|
|
|
26621
26635
|
out.userSubType = m.userSubType;
|
|
26622
26636
|
return out;
|
|
26623
26637
|
};
|
|
26624
|
-
var
|
|
26625
|
-
const
|
|
26626
|
-
|
|
26627
|
-
|
|
26628
|
-
|
|
26629
|
-
|
|
26630
|
-
|
|
26631
|
-
|
|
26632
|
-
|
|
26638
|
+
var isNameResolvable = (m) => {
|
|
26639
|
+
const fields = [m.displayName, m.email, m.userPrincipalName, m.givenName, m.surname];
|
|
26640
|
+
return fields.some((f) => typeof f === "string" && f.trim() !== "");
|
|
26641
|
+
};
|
|
26642
|
+
var bareMemberCount = (members) => members.filter((m) => !isNameResolvable(m)).length;
|
|
26643
|
+
var fromGraphMember = (g) => ({
|
|
26644
|
+
...g.displayName !== undefined ? { displayName: g.displayName } : {},
|
|
26645
|
+
...g.email !== undefined ? { email: g.email } : {},
|
|
26646
|
+
...typeof g.userId === "string" ? { mri: `8:orgid:${g.userId}`, objectId: g.userId } : {}
|
|
26647
|
+
});
|
|
26648
|
+
var toMatchedChat = (chatId, chat, members) => ({
|
|
26649
|
+
chatId,
|
|
26650
|
+
title: chat.title ?? null,
|
|
26651
|
+
chatType: chat.chatType,
|
|
26652
|
+
threadType: chat.threadType,
|
|
26653
|
+
memberCount: (chat.members ?? []).length,
|
|
26654
|
+
...chat.lastMessage?.composeTime !== undefined ? { lastMessageAt: chat.lastMessage.composeTime } : {},
|
|
26655
|
+
matchedMembers: members.map(projectMember)
|
|
26656
|
+
});
|
|
26657
|
+
var collectChat = (chat, queryFolded, acc) => {
|
|
26658
|
+
if (chat.id === undefined || acc.seen.has(chat.id))
|
|
26659
|
+
return;
|
|
26660
|
+
const members = chat.members ?? [];
|
|
26661
|
+
const hits = members.filter((m) => memberMatches(m, queryFolded));
|
|
26662
|
+
if (hits.length > 0) {
|
|
26663
|
+
acc.seen.add(chat.id);
|
|
26664
|
+
acc.matched.push(toMatchedChat(chat.id, chat, hits));
|
|
26665
|
+
return;
|
|
26666
|
+
}
|
|
26667
|
+
const bareCount = bareMemberCount(members);
|
|
26668
|
+
if (bareCount > 0) {
|
|
26669
|
+
acc.seen.add(chat.id);
|
|
26670
|
+
acc.bareUnmatched.push({ chatId: chat.id, chat, bareCount });
|
|
26671
|
+
}
|
|
26672
|
+
};
|
|
26673
|
+
var scanChatPages = async (graph, queryFolded, pageSize, maxPages) => {
|
|
26674
|
+
const acc = { seen: new Set, matched: [], bareUnmatched: [] };
|
|
26633
26675
|
let continuationToken;
|
|
26634
26676
|
let pagesFetched = 0;
|
|
26635
26677
|
let chatsScanned = 0;
|
|
@@ -26644,42 +26686,67 @@ var execute107 = async (graph, params) => {
|
|
|
26644
26686
|
const chats = body.chats ?? [];
|
|
26645
26687
|
pagesFetched += 1;
|
|
26646
26688
|
chatsScanned += chats.length;
|
|
26647
|
-
for (const chat of chats)
|
|
26648
|
-
|
|
26649
|
-
continue;
|
|
26650
|
-
const members = chat.members ?? [];
|
|
26651
|
-
const matchedMembers = members.filter((m) => memberMatches(m, queryFolded));
|
|
26652
|
-
if (matchedMembers.length === 0)
|
|
26653
|
-
continue;
|
|
26654
|
-
seenChatIds.add(chat.id);
|
|
26655
|
-
matched.push({
|
|
26656
|
-
chatId: chat.id,
|
|
26657
|
-
title: chat.title ?? null,
|
|
26658
|
-
chatType: chat.chatType,
|
|
26659
|
-
threadType: chat.threadType,
|
|
26660
|
-
memberCount: members.length,
|
|
26661
|
-
...chat.lastMessage?.composeTime !== undefined ? { lastMessageAt: chat.lastMessage.composeTime } : {},
|
|
26662
|
-
matchedMembers: matchedMembers.map(projectMember)
|
|
26663
|
-
});
|
|
26664
|
-
}
|
|
26689
|
+
for (const chat of chats)
|
|
26690
|
+
collectChat(chat, queryFolded, acc);
|
|
26665
26691
|
if (body.hasMoreData !== true || body.continuationToken === undefined) {
|
|
26666
26692
|
continuationToken = undefined;
|
|
26667
26693
|
break;
|
|
26668
26694
|
}
|
|
26669
26695
|
continuationToken = body.continuationToken;
|
|
26670
26696
|
}
|
|
26697
|
+
return ok({ matched: acc.matched, bareUnmatched: acc.bareUnmatched, pagesFetched, chatsScanned, continuationToken });
|
|
26698
|
+
};
|
|
26699
|
+
var sumBareCounts = (bare) => bare.reduce((n, b) => n + b.bareCount, 0);
|
|
26700
|
+
var hydrateMatches = async (graph, chatId, queryFolded) => {
|
|
26701
|
+
const res = await graph.get(`/chats/${chatId}/members`);
|
|
26702
|
+
if (!res.ok)
|
|
26703
|
+
return null;
|
|
26704
|
+
const value = res.value.value ?? [];
|
|
26705
|
+
return value.map(fromGraphMember).filter((m) => memberMatches(m, queryFolded));
|
|
26706
|
+
};
|
|
26707
|
+
var isDirectChat = (chatId) => chatId.endsWith("@unq.gbl.spaces");
|
|
26708
|
+
var hydrateBareDirect = async (graph, queryFolded, bareUnmatched, matched) => {
|
|
26709
|
+
const direct = bareUnmatched.filter((b) => isDirectChat(b.chatId));
|
|
26710
|
+
let unresolvedMemberCount = sumBareCounts(bareUnmatched.filter((b) => !isDirectChat(b.chatId)));
|
|
26711
|
+
const results = await Promise.all(direct.map(async (b) => ({ bare: b, hits: await hydrateMatches(graph, b.chatId, queryFolded) })));
|
|
26712
|
+
for (const { bare, hits } of results) {
|
|
26713
|
+
if (hits === null) {
|
|
26714
|
+
unresolvedMemberCount += bare.bareCount;
|
|
26715
|
+
continue;
|
|
26716
|
+
}
|
|
26717
|
+
if (hits.length > 0)
|
|
26718
|
+
matched.push(toMatchedChat(bare.chatId, bare.chat, hits));
|
|
26719
|
+
}
|
|
26720
|
+
return { chatsHydrated: direct.length, unresolvedMemberCount };
|
|
26721
|
+
};
|
|
26722
|
+
var HINT2 = "No chat member matched by name, but at least one chat has a cross-tenant member the Teams roster left unresolved — an externally-homed counterpart often appears only as a bare object-id. Direct 1:1 chats were deep-probed; members in group/meeting chats were not. Retry searching by their object-id (pass it as `--name <object-id>`), or, if you have the chat URL, read it directly with `get-chat` / `list-teams-chat-messages --chat-id 19:<their-oid>_<your-oid>@unq.gbl.spaces`.";
|
|
26723
|
+
var execute107 = async (graph, params) => {
|
|
26724
|
+
const parsed = schema107.safeParse(params);
|
|
26725
|
+
if (!parsed.success)
|
|
26726
|
+
return err({ type: "validation_error", message: formatZodError(parsed.error) });
|
|
26727
|
+
const queryFolded = fold(parsed.data.name);
|
|
26728
|
+
const pageSize = parsed.data.pageSize ?? "100";
|
|
26729
|
+
const maxPages = Number(parsed.data.maxPages ?? "20");
|
|
26730
|
+
const scan = await scanChatPages(graph, queryFolded, pageSize, maxPages);
|
|
26731
|
+
if (!scan.ok)
|
|
26732
|
+
return scan;
|
|
26733
|
+
const { matched, bareUnmatched, pagesFetched, chatsScanned, continuationToken } = scan.value;
|
|
26734
|
+
const { chatsHydrated, unresolvedMemberCount } = await hydrateBareDirect(graph, queryFolded, bareUnmatched, matched);
|
|
26671
26735
|
return ok({
|
|
26672
26736
|
name: parsed.data.name,
|
|
26673
26737
|
matches: matched,
|
|
26674
26738
|
matchCount: matched.length,
|
|
26675
26739
|
pagesFetched,
|
|
26676
26740
|
chatsScanned,
|
|
26741
|
+
chatsHydrated,
|
|
26742
|
+
unresolvedMemberCount,
|
|
26677
26743
|
hasMore: continuationToken !== undefined,
|
|
26678
|
-
nextContinuationToken: continuationToken
|
|
26744
|
+
nextContinuationToken: continuationToken,
|
|
26745
|
+
...matched.length === 0 && unresolvedMemberCount > 0 ? { hint: HINT2 } : {}
|
|
26679
26746
|
});
|
|
26680
26747
|
};
|
|
26681
26748
|
var meta109 = {
|
|
26682
|
-
summary: 'Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical "all conversations with person X" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API.',
|
|
26749
|
+
summary: 'Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical "all conversations with person X" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). Cross-tenant resolution: the summary roster returns externally-homed counterparts as a bare object-id (no name/email), which a name search cannot match; for every bare DIRECT (1:1) chat the command hydrates the roster via the per-chat members endpoint and re-matches — so an external counterpart who is bare in your 1:1 is still found, even when they were already resolved in some meeting (the dual-identity case). Bare members in group/meeting chats are not deep-probed; when nothing matches and such members exist it returns a `hint` plus `unresolvedMemberCount` rather than a confident empty result. **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API.',
|
|
26683
26750
|
category: "chats",
|
|
26684
26751
|
needsSubstrateToken: true,
|
|
26685
26752
|
graphMethod: "GET",
|
|
@@ -26696,7 +26763,7 @@ var meta109 = {
|
|
|
26696
26763
|
name: "max-pages",
|
|
26697
26764
|
key: "maxPages",
|
|
26698
26765
|
required: false,
|
|
26699
|
-
description: "Safety cap on the chat-list walk (positive integer; default
|
|
26766
|
+
description: "Safety cap on the chat-list walk (positive integer; default 20). The walk stops early once the substrate reports no more pages, so the cap only bites on accounts with more chats than `--max-pages × --page-size`; raise it (and watch `hasMore`) if a known chat is missed. Each page is one HTTP round-trip."
|
|
26700
26767
|
},
|
|
26701
26768
|
{
|
|
26702
26769
|
name: "page-size",
|
|
@@ -26706,7 +26773,7 @@ var meta109 = {
|
|
|
26706
26773
|
}
|
|
26707
26774
|
],
|
|
26708
26775
|
example: "ask-marcel find-chats-with-user --name 'Jane DOE'",
|
|
26709
|
-
responseShape: "`{ name, matches: [{ chatId, title, chatType, threadType, memberCount, lastMessageAt?, matchedMembers: [{ mri, displayName, email, userSubType }] }], matchCount, pagesFetched, chatsScanned, hasMore, nextContinuationToken? }`. `matchedMembers` always carries the matching entries' identifying fields — pass `chatId` into `list-teams-chat-history` to read message bodies. `hasMore: true` means `--max-pages` was hit before exhausting the chat list; chain with the existing `--continuation-token` flag on `list-teams-chats-with-messages` if you need to scan further (this command does not advertise a `--continuation-token` because resuming a partial search is rare; users either widen `--max-pages` or refine `--name`).",
|
|
26776
|
+
responseShape: "`{ name, matches: [{ chatId, title, chatType, threadType, memberCount, lastMessageAt?, matchedMembers: [{ mri, displayName, email, userSubType }] }], matchCount, pagesFetched, chatsScanned, chatsHydrated, unresolvedMemberCount, hasMore, nextContinuationToken?, hint? }`. `matchedMembers` always carries the matching entries' identifying fields — pass `chatId` into `list-teams-chat-history` to read message bodies. `chatsHydrated` counts the per-chat members lookups spent resolving bare cross-tenant members in direct (1:1) chats. `unresolvedMemberCount` is how many cross-tenant members are still unresolved by name (bare members in group/meeting chats, which are not deep-probed, plus any 1:1 hydration that errored); when `matchCount` is 0 and this is non-zero, a `hint` is present explaining the likely cause and the object-id / read-by-chat-id remedy — so an empty result is never silently confident. `hasMore: true` means `--max-pages` was hit before exhausting the chat list; chain with the existing `--continuation-token` flag on `list-teams-chats-with-messages` if you need to scan further (this command does not advertise a `--continuation-token` because resuming a partial search is rare; users either widen `--max-pages` or refine `--name`).",
|
|
26710
26777
|
stability: "experimental"
|
|
26711
26778
|
};
|
|
26712
26779
|
|
package/dist/commands.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "ask-marcel-office-cli",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"generatedAt": "2026-06-
|
|
3
|
+
"version": "1.5.2",
|
|
4
|
+
"generatedAt": "2026-06-23T11:35:14.255Z",
|
|
5
5
|
"commands": [
|
|
6
6
|
{
|
|
7
7
|
"name": "convert-calendar-event-attachment-to-markdown",
|
|
@@ -666,7 +666,7 @@
|
|
|
666
666
|
},
|
|
667
667
|
{
|
|
668
668
|
"name": "find-chats-with-user",
|
|
669
|
-
"summary": "Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical \"all conversations with person X\" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API.",
|
|
669
|
+
"summary": "Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical \"all conversations with person X\" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). Cross-tenant resolution: the summary roster returns externally-homed counterparts as a bare object-id (no name/email), which a name search cannot match; for every bare DIRECT (1:1) chat the command hydrates the roster via the per-chat members endpoint and re-matches — so an external counterpart who is bare in your 1:1 is still found, even when they were already resolved in some meeting (the dual-identity case). Bare members in group/meeting chats are not deep-probed; when nothing matches and such members exist it returns a `hint` plus `unresolvedMemberCount` rather than a confident empty result. **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API.",
|
|
670
670
|
"category": "chats",
|
|
671
671
|
"graphMethod": "GET",
|
|
672
672
|
"graphPathTemplate": "https://teams.microsoft.com/api/csa/{region}/api/v3/teams/users/me/chats",
|
|
@@ -682,7 +682,7 @@
|
|
|
682
682
|
"name": "max-pages",
|
|
683
683
|
"key": "maxPages",
|
|
684
684
|
"required": false,
|
|
685
|
-
"description": "Safety cap on the chat-list walk (positive integer; default
|
|
685
|
+
"description": "Safety cap on the chat-list walk (positive integer; default 20). The walk stops early once the substrate reports no more pages, so the cap only bites on accounts with more chats than `--max-pages × --page-size`; raise it (and watch `hasMore`) if a known chat is missed. Each page is one HTTP round-trip."
|
|
686
686
|
},
|
|
687
687
|
{
|
|
688
688
|
"name": "page-size",
|
|
@@ -692,7 +692,7 @@
|
|
|
692
692
|
}
|
|
693
693
|
],
|
|
694
694
|
"example": "ask-marcel find-chats-with-user --name 'Jane DOE'",
|
|
695
|
-
"responseShape": "`{ name, matches: [{ chatId, title, chatType, threadType, memberCount, lastMessageAt?, matchedMembers: [{ mri, displayName, email, userSubType }] }], matchCount, pagesFetched, chatsScanned, hasMore, nextContinuationToken? }`. `matchedMembers` always carries the matching entries' identifying fields — pass `chatId` into `list-teams-chat-history` to read message bodies. `hasMore: true` means `--max-pages` was hit before exhausting the chat list; chain with the existing `--continuation-token` flag on `list-teams-chats-with-messages` if you need to scan further (this command does not advertise a `--continuation-token` because resuming a partial search is rare; users either widen `--max-pages` or refine `--name`).",
|
|
695
|
+
"responseShape": "`{ name, matches: [{ chatId, title, chatType, threadType, memberCount, lastMessageAt?, matchedMembers: [{ mri, displayName, email, userSubType }] }], matchCount, pagesFetched, chatsScanned, chatsHydrated, unresolvedMemberCount, hasMore, nextContinuationToken?, hint? }`. `matchedMembers` always carries the matching entries' identifying fields — pass `chatId` into `list-teams-chat-history` to read message bodies. `chatsHydrated` counts the per-chat members lookups spent resolving bare cross-tenant members in direct (1:1) chats. `unresolvedMemberCount` is how many cross-tenant members are still unresolved by name (bare members in group/meeting chats, which are not deep-probed, plus any 1:1 hydration that errored); when `matchCount` is 0 and this is non-zero, a `hint` is present explaining the likely cause and the object-id / read-by-chat-id remedy — so an empty result is never silently confident. `hasMore: true` means `--max-pages` was hit before exhausting the chat list; chain with the existing `--continuation-token` flag on `list-teams-chats-with-messages` if you need to scan further (this command does not advertise a `--continuation-token` because resuming a partial search is rare; users either widen `--max-pages` or refine `--name`).",
|
|
696
696
|
"needsSubstrateToken": true,
|
|
697
697
|
"stability": "experimental"
|
|
698
698
|
},
|
|
@@ -2987,8 +2987,7 @@
|
|
|
2987
2987
|
],
|
|
2988
2988
|
"example": "ask-marcel list-chat-members --chat-id '19:abc...@thread.v2'",
|
|
2989
2989
|
"responseShape": "collection of Microsoft Graph `conversationMember` resources under `value[]`",
|
|
2990
|
-
"pagination": true
|
|
2991
|
-
"needsElevatedToken": true
|
|
2990
|
+
"pagination": true
|
|
2992
2991
|
},
|
|
2993
2992
|
{
|
|
2994
2993
|
"name": "list-chats",
|
package/dist/index.js
CHANGED
|
@@ -1249,7 +1249,8 @@ var SCOPES = "https://graph.microsoft.com/.default openid profile offline_access
|
|
|
1249
1249
|
var SPA_ORIGIN = "https://teams.microsoft.com";
|
|
1250
1250
|
var TEAMS_URL2 = "https://teams.microsoft.com/";
|
|
1251
1251
|
var DEFAULT_CHATSVCAGG_REGION2 = "emea";
|
|
1252
|
-
var
|
|
1252
|
+
var failFastSecondaryMessage = (token, commands) => `${token} token is expired or was not captured at login. Run \`ask-marcel login\` to (re)capture it — the CLI does not open a browser per command for this token. (Commands that need it: ${commands}.)`;
|
|
1253
|
+
var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logger, fs, systemBrowserAuthFn, usePlaywrightFallback = true, skipSystemBrowser = false, recaptureSecondaryViaBrowser = true) => {
|
|
1253
1254
|
const systemBrowserAuth = systemBrowserAuthFn ?? defaultSystemBrowserAuth(logger, skipSystemBrowser);
|
|
1254
1255
|
const readCache = async () => {
|
|
1255
1256
|
const r = await fs.readJson(cachePath);
|
|
@@ -1431,12 +1432,12 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
1431
1432
|
};
|
|
1432
1433
|
const recoverableElevatedFailureMessage = (reason) => {
|
|
1433
1434
|
if (reason === "launch_timeout") {
|
|
1434
|
-
return "elevated browser launch timed out (15s) — likely a corrupt persistent profile or filesystem lock. Run `ask-marcel logout && ask-marcel login` to wipe the profile and retry. (Commands that need this token: list-chats, get-chat,
|
|
1435
|
+
return "elevated browser launch timed out (15s) — likely a corrupt persistent profile or filesystem lock. Run `ask-marcel logout && ask-marcel login` to wipe the profile and retry. (Commands that need this token: list-chats, get-chat, the historical-version download / convert commands.)";
|
|
1435
1436
|
}
|
|
1436
1437
|
if (reason === "navigation_failed") {
|
|
1437
|
-
return "elevated capture failed: navigation to m365.cloud.microsoft did not complete — network issue, corp-proxy block, or tenant policy. Check connectivity and retry. If persistent, the elevated commands (list-chats / get-chat /
|
|
1438
|
+
return "elevated capture failed: navigation to m365.cloud.microsoft did not complete — network issue, corp-proxy block, or tenant policy. Check connectivity and retry. If persistent, the elevated commands (list-chats / get-chat / historical-version downloads) will be unavailable.";
|
|
1438
1439
|
}
|
|
1439
|
-
return "elevated token capture timed out — silent SSO against m365.cloud.microsoft did not yield a Bearer within 20s. The persistent browser-profile cookies are likely expired. Run `ask-marcel logout && ask-marcel login` — this now wipes the profile too. (Commands that need this token: list-chats, get-chat,
|
|
1440
|
+
return "elevated token capture timed out — silent SSO against m365.cloud.microsoft did not yield a Bearer within 20s. The persistent browser-profile cookies are likely expired. Run `ask-marcel logout && ask-marcel login` — this now wipes the profile too. (Commands that need this token: list-chats, get-chat, the historical-version download / convert commands.)";
|
|
1440
1441
|
};
|
|
1441
1442
|
const recaptureElevated = async () => {
|
|
1442
1443
|
try {
|
|
@@ -1471,6 +1472,8 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
1471
1472
|
logger.info("auth.elevated.cache_hit");
|
|
1472
1473
|
return ok(validated.value);
|
|
1473
1474
|
}
|
|
1475
|
+
if (!recaptureSecondaryViaBrowser)
|
|
1476
|
+
return err({ type: "auth_failed", message: failFastSecondaryMessage("Elevated (M365)", "list-chats, get-chat, download-drive-item-version") });
|
|
1474
1477
|
return recaptureElevatedShared();
|
|
1475
1478
|
};
|
|
1476
1479
|
const freshChatsvcaggToken = (cached) => {
|
|
@@ -1521,6 +1524,11 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
1521
1524
|
logger.info("auth.chatsvcagg.cache_hit");
|
|
1522
1525
|
return ok(accessTokenUnsafe(fresh));
|
|
1523
1526
|
}
|
|
1527
|
+
if (!recaptureSecondaryViaBrowser)
|
|
1528
|
+
return err({
|
|
1529
|
+
type: "auth_failed",
|
|
1530
|
+
message: failFastSecondaryMessage("chatsvcagg (Teams chat)", "list-teams-chats-with-messages, list-teams-chat-messages, get-teams-chat-message, find-chats-with-user")
|
|
1531
|
+
});
|
|
1524
1532
|
return recaptureChatsvcaggShared();
|
|
1525
1533
|
};
|
|
1526
1534
|
const getChatsvcaggRegion = async () => {
|
|
@@ -1576,6 +1584,8 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
1576
1584
|
logger.info("auth.ic3.cache_hit");
|
|
1577
1585
|
return ok(accessTokenUnsafe(fresh));
|
|
1578
1586
|
}
|
|
1587
|
+
if (!recaptureSecondaryViaBrowser)
|
|
1588
|
+
return err({ type: "auth_failed", message: failFastSecondaryMessage("ic3 (Teams chat history)", "list-teams-chat-history") });
|
|
1579
1589
|
return recaptureIc3Shared();
|
|
1580
1590
|
};
|
|
1581
1591
|
const logout = async () => {
|
|
@@ -1633,7 +1643,7 @@ var createAuthManager = (deps) => {
|
|
|
1633
1643
|
freshCachedToken: createFreshCachedTokenProbe(fs, deps.cachePath),
|
|
1634
1644
|
onProgress: stderrProgress
|
|
1635
1645
|
});
|
|
1636
|
-
return createAuthManagerFromApi(browserAuth, deps.cachePath, browserProfileDir, deps.logger, fs, deps.systemBrowserAuth, deps.usePlaywrightFallback, deps.skipSystemBrowser);
|
|
1646
|
+
return createAuthManagerFromApi(browserAuth, deps.cachePath, browserProfileDir, deps.logger, fs, deps.systemBrowserAuth, deps.usePlaywrightFallback, deps.skipSystemBrowser, deps.recaptureSecondaryViaBrowser);
|
|
1637
1647
|
};
|
|
1638
1648
|
// src/infra/network-error.ts
|
|
1639
1649
|
var REQUEST_TIMEOUT_MS = 60000;
|
|
@@ -19896,7 +19906,7 @@ __export(exports_list_chat_members, {
|
|
|
19896
19906
|
});
|
|
19897
19907
|
var baseSchema21 = exports_external.object({ chatId: exports_external.string().min(1) });
|
|
19898
19908
|
var CHAT_MEMBERS_ODATA_KEYS = ["skip", "select", "filter"];
|
|
19899
|
-
var inner5 =
|
|
19909
|
+
var inner5 = buildPickODataListCommand((p) => `/chats/${p.chatId}/members`, baseSchema21, CHAT_MEMBERS_ODATA_KEYS);
|
|
19900
19910
|
var execute42 = async (graph, params) => {
|
|
19901
19911
|
const result = await inner5.execute(graph, params);
|
|
19902
19912
|
if (result.ok)
|
|
@@ -19931,8 +19941,7 @@ var meta44 = {
|
|
|
19931
19941
|
],
|
|
19932
19942
|
example: "ask-marcel list-chat-members --chat-id '19:abc...@thread.v2'",
|
|
19933
19943
|
responseShape: "collection of Microsoft Graph `conversationMember` resources under `value[]`",
|
|
19934
|
-
pagination: true
|
|
19935
|
-
needsElevatedToken: true
|
|
19944
|
+
pagination: true
|
|
19936
19945
|
};
|
|
19937
19946
|
|
|
19938
19947
|
// src/use-cases/commands/list-drive-item-permissions.ts
|
|
@@ -20894,8 +20903,13 @@ __export(exports_next_page, {
|
|
|
20894
20903
|
execute: () => execute74
|
|
20895
20904
|
});
|
|
20896
20905
|
var PREFIX = "https://graph.microsoft.com/v1.0";
|
|
20897
|
-
var
|
|
20898
|
-
|
|
20906
|
+
var requiresElevated = (path) => {
|
|
20907
|
+
if (path.startsWith("/me/chats"))
|
|
20908
|
+
return true;
|
|
20909
|
+
if (path.startsWith("/chats/"))
|
|
20910
|
+
return !path.includes("/members");
|
|
20911
|
+
return false;
|
|
20912
|
+
};
|
|
20899
20913
|
var schema74 = exports_external.object({
|
|
20900
20914
|
url: exports_external.string().min(1).refine((v) => v.startsWith(`${PREFIX}/`), { message: `must be a Microsoft Graph v1.0 URL starting with ${PREFIX}/` })
|
|
20901
20915
|
});
|
|
@@ -23513,15 +23527,43 @@ var projectMember = (m) => {
|
|
|
23513
23527
|
out.userSubType = m.userSubType;
|
|
23514
23528
|
return out;
|
|
23515
23529
|
};
|
|
23516
|
-
var
|
|
23517
|
-
const
|
|
23518
|
-
|
|
23519
|
-
|
|
23520
|
-
|
|
23521
|
-
|
|
23522
|
-
|
|
23523
|
-
|
|
23524
|
-
|
|
23530
|
+
var isNameResolvable = (m) => {
|
|
23531
|
+
const fields = [m.displayName, m.email, m.userPrincipalName, m.givenName, m.surname];
|
|
23532
|
+
return fields.some((f) => typeof f === "string" && f.trim() !== "");
|
|
23533
|
+
};
|
|
23534
|
+
var bareMemberCount = (members) => members.filter((m) => !isNameResolvable(m)).length;
|
|
23535
|
+
var fromGraphMember = (g) => ({
|
|
23536
|
+
...g.displayName !== undefined ? { displayName: g.displayName } : {},
|
|
23537
|
+
...g.email !== undefined ? { email: g.email } : {},
|
|
23538
|
+
...typeof g.userId === "string" ? { mri: `8:orgid:${g.userId}`, objectId: g.userId } : {}
|
|
23539
|
+
});
|
|
23540
|
+
var toMatchedChat = (chatId, chat, members) => ({
|
|
23541
|
+
chatId,
|
|
23542
|
+
title: chat.title ?? null,
|
|
23543
|
+
chatType: chat.chatType,
|
|
23544
|
+
threadType: chat.threadType,
|
|
23545
|
+
memberCount: (chat.members ?? []).length,
|
|
23546
|
+
...chat.lastMessage?.composeTime !== undefined ? { lastMessageAt: chat.lastMessage.composeTime } : {},
|
|
23547
|
+
matchedMembers: members.map(projectMember)
|
|
23548
|
+
});
|
|
23549
|
+
var collectChat = (chat, queryFolded, acc) => {
|
|
23550
|
+
if (chat.id === undefined || acc.seen.has(chat.id))
|
|
23551
|
+
return;
|
|
23552
|
+
const members = chat.members ?? [];
|
|
23553
|
+
const hits = members.filter((m) => memberMatches(m, queryFolded));
|
|
23554
|
+
if (hits.length > 0) {
|
|
23555
|
+
acc.seen.add(chat.id);
|
|
23556
|
+
acc.matched.push(toMatchedChat(chat.id, chat, hits));
|
|
23557
|
+
return;
|
|
23558
|
+
}
|
|
23559
|
+
const bareCount = bareMemberCount(members);
|
|
23560
|
+
if (bareCount > 0) {
|
|
23561
|
+
acc.seen.add(chat.id);
|
|
23562
|
+
acc.bareUnmatched.push({ chatId: chat.id, chat, bareCount });
|
|
23563
|
+
}
|
|
23564
|
+
};
|
|
23565
|
+
var scanChatPages = async (graph, queryFolded, pageSize, maxPages) => {
|
|
23566
|
+
const acc = { seen: new Set, matched: [], bareUnmatched: [] };
|
|
23525
23567
|
let continuationToken;
|
|
23526
23568
|
let pagesFetched = 0;
|
|
23527
23569
|
let chatsScanned = 0;
|
|
@@ -23536,42 +23578,67 @@ var execute107 = async (graph, params) => {
|
|
|
23536
23578
|
const chats = body.chats ?? [];
|
|
23537
23579
|
pagesFetched += 1;
|
|
23538
23580
|
chatsScanned += chats.length;
|
|
23539
|
-
for (const chat of chats)
|
|
23540
|
-
|
|
23541
|
-
continue;
|
|
23542
|
-
const members = chat.members ?? [];
|
|
23543
|
-
const matchedMembers = members.filter((m) => memberMatches(m, queryFolded));
|
|
23544
|
-
if (matchedMembers.length === 0)
|
|
23545
|
-
continue;
|
|
23546
|
-
seenChatIds.add(chat.id);
|
|
23547
|
-
matched.push({
|
|
23548
|
-
chatId: chat.id,
|
|
23549
|
-
title: chat.title ?? null,
|
|
23550
|
-
chatType: chat.chatType,
|
|
23551
|
-
threadType: chat.threadType,
|
|
23552
|
-
memberCount: members.length,
|
|
23553
|
-
...chat.lastMessage?.composeTime !== undefined ? { lastMessageAt: chat.lastMessage.composeTime } : {},
|
|
23554
|
-
matchedMembers: matchedMembers.map(projectMember)
|
|
23555
|
-
});
|
|
23556
|
-
}
|
|
23581
|
+
for (const chat of chats)
|
|
23582
|
+
collectChat(chat, queryFolded, acc);
|
|
23557
23583
|
if (body.hasMoreData !== true || body.continuationToken === undefined) {
|
|
23558
23584
|
continuationToken = undefined;
|
|
23559
23585
|
break;
|
|
23560
23586
|
}
|
|
23561
23587
|
continuationToken = body.continuationToken;
|
|
23562
23588
|
}
|
|
23589
|
+
return ok({ matched: acc.matched, bareUnmatched: acc.bareUnmatched, pagesFetched, chatsScanned, continuationToken });
|
|
23590
|
+
};
|
|
23591
|
+
var sumBareCounts = (bare) => bare.reduce((n, b) => n + b.bareCount, 0);
|
|
23592
|
+
var hydrateMatches = async (graph, chatId, queryFolded) => {
|
|
23593
|
+
const res = await graph.get(`/chats/${chatId}/members`);
|
|
23594
|
+
if (!res.ok)
|
|
23595
|
+
return null;
|
|
23596
|
+
const value = res.value.value ?? [];
|
|
23597
|
+
return value.map(fromGraphMember).filter((m) => memberMatches(m, queryFolded));
|
|
23598
|
+
};
|
|
23599
|
+
var isDirectChat = (chatId) => chatId.endsWith("@unq.gbl.spaces");
|
|
23600
|
+
var hydrateBareDirect = async (graph, queryFolded, bareUnmatched, matched) => {
|
|
23601
|
+
const direct = bareUnmatched.filter((b) => isDirectChat(b.chatId));
|
|
23602
|
+
let unresolvedMemberCount = sumBareCounts(bareUnmatched.filter((b) => !isDirectChat(b.chatId)));
|
|
23603
|
+
const results = await Promise.all(direct.map(async (b) => ({ bare: b, hits: await hydrateMatches(graph, b.chatId, queryFolded) })));
|
|
23604
|
+
for (const { bare, hits } of results) {
|
|
23605
|
+
if (hits === null) {
|
|
23606
|
+
unresolvedMemberCount += bare.bareCount;
|
|
23607
|
+
continue;
|
|
23608
|
+
}
|
|
23609
|
+
if (hits.length > 0)
|
|
23610
|
+
matched.push(toMatchedChat(bare.chatId, bare.chat, hits));
|
|
23611
|
+
}
|
|
23612
|
+
return { chatsHydrated: direct.length, unresolvedMemberCount };
|
|
23613
|
+
};
|
|
23614
|
+
var HINT2 = "No chat member matched by name, but at least one chat has a cross-tenant member the Teams roster left unresolved — an externally-homed counterpart often appears only as a bare object-id. Direct 1:1 chats were deep-probed; members in group/meeting chats were not. Retry searching by their object-id (pass it as `--name <object-id>`), or, if you have the chat URL, read it directly with `get-chat` / `list-teams-chat-messages --chat-id 19:<their-oid>_<your-oid>@unq.gbl.spaces`.";
|
|
23615
|
+
var execute107 = async (graph, params) => {
|
|
23616
|
+
const parsed = schema107.safeParse(params);
|
|
23617
|
+
if (!parsed.success)
|
|
23618
|
+
return err({ type: "validation_error", message: formatZodError(parsed.error) });
|
|
23619
|
+
const queryFolded = fold(parsed.data.name);
|
|
23620
|
+
const pageSize = parsed.data.pageSize ?? "100";
|
|
23621
|
+
const maxPages = Number(parsed.data.maxPages ?? "20");
|
|
23622
|
+
const scan = await scanChatPages(graph, queryFolded, pageSize, maxPages);
|
|
23623
|
+
if (!scan.ok)
|
|
23624
|
+
return scan;
|
|
23625
|
+
const { matched, bareUnmatched, pagesFetched, chatsScanned, continuationToken } = scan.value;
|
|
23626
|
+
const { chatsHydrated, unresolvedMemberCount } = await hydrateBareDirect(graph, queryFolded, bareUnmatched, matched);
|
|
23563
23627
|
return ok({
|
|
23564
23628
|
name: parsed.data.name,
|
|
23565
23629
|
matches: matched,
|
|
23566
23630
|
matchCount: matched.length,
|
|
23567
23631
|
pagesFetched,
|
|
23568
23632
|
chatsScanned,
|
|
23633
|
+
chatsHydrated,
|
|
23634
|
+
unresolvedMemberCount,
|
|
23569
23635
|
hasMore: continuationToken !== undefined,
|
|
23570
|
-
nextContinuationToken: continuationToken
|
|
23636
|
+
nextContinuationToken: continuationToken,
|
|
23637
|
+
...matched.length === 0 && unresolvedMemberCount > 0 ? { hint: HINT2 } : {}
|
|
23571
23638
|
});
|
|
23572
23639
|
};
|
|
23573
23640
|
var meta109 = {
|
|
23574
|
-
summary: 'Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical "all conversations with person X" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API.',
|
|
23641
|
+
summary: 'Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical "all conversations with person X" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). Cross-tenant resolution: the summary roster returns externally-homed counterparts as a bare object-id (no name/email), which a name search cannot match; for every bare DIRECT (1:1) chat the command hydrates the roster via the per-chat members endpoint and re-matches — so an external counterpart who is bare in your 1:1 is still found, even when they were already resolved in some meeting (the dual-identity case). Bare members in group/meeting chats are not deep-probed; when nothing matches and such members exist it returns a `hint` plus `unresolvedMemberCount` rather than a confident empty result. **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API.',
|
|
23575
23642
|
category: "chats",
|
|
23576
23643
|
needsSubstrateToken: true,
|
|
23577
23644
|
graphMethod: "GET",
|
|
@@ -23588,7 +23655,7 @@ var meta109 = {
|
|
|
23588
23655
|
name: "max-pages",
|
|
23589
23656
|
key: "maxPages",
|
|
23590
23657
|
required: false,
|
|
23591
|
-
description: "Safety cap on the chat-list walk (positive integer; default
|
|
23658
|
+
description: "Safety cap on the chat-list walk (positive integer; default 20). The walk stops early once the substrate reports no more pages, so the cap only bites on accounts with more chats than `--max-pages × --page-size`; raise it (and watch `hasMore`) if a known chat is missed. Each page is one HTTP round-trip."
|
|
23592
23659
|
},
|
|
23593
23660
|
{
|
|
23594
23661
|
name: "page-size",
|
|
@@ -23598,7 +23665,7 @@ var meta109 = {
|
|
|
23598
23665
|
}
|
|
23599
23666
|
],
|
|
23600
23667
|
example: "ask-marcel find-chats-with-user --name 'Jane DOE'",
|
|
23601
|
-
responseShape: "`{ name, matches: [{ chatId, title, chatType, threadType, memberCount, lastMessageAt?, matchedMembers: [{ mri, displayName, email, userSubType }] }], matchCount, pagesFetched, chatsScanned, hasMore, nextContinuationToken? }`. `matchedMembers` always carries the matching entries' identifying fields — pass `chatId` into `list-teams-chat-history` to read message bodies. `hasMore: true` means `--max-pages` was hit before exhausting the chat list; chain with the existing `--continuation-token` flag on `list-teams-chats-with-messages` if you need to scan further (this command does not advertise a `--continuation-token` because resuming a partial search is rare; users either widen `--max-pages` or refine `--name`).",
|
|
23668
|
+
responseShape: "`{ name, matches: [{ chatId, title, chatType, threadType, memberCount, lastMessageAt?, matchedMembers: [{ mri, displayName, email, userSubType }] }], matchCount, pagesFetched, chatsScanned, chatsHydrated, unresolvedMemberCount, hasMore, nextContinuationToken?, hint? }`. `matchedMembers` always carries the matching entries' identifying fields — pass `chatId` into `list-teams-chat-history` to read message bodies. `chatsHydrated` counts the per-chat members lookups spent resolving bare cross-tenant members in direct (1:1) chats. `unresolvedMemberCount` is how many cross-tenant members are still unresolved by name (bare members in group/meeting chats, which are not deep-probed, plus any 1:1 hydration that errored); when `matchCount` is 0 and this is non-zero, a `hint` is present explaining the likely cause and the object-id / read-by-chat-id remedy — so an empty result is never silently confident. `hasMore: true` means `--max-pages` was hit before exhausting the chat list; chain with the existing `--continuation-token` flag on `list-teams-chats-with-messages` if you need to scan further (this command does not advertise a `--continuation-token` because resuming a partial search is rare; users either widen `--max-pages` or refine `--name`).",
|
|
23602
23669
|
stability: "experimental"
|
|
23603
23670
|
};
|
|
23604
23671
|
|
|
@@ -26018,7 +26085,7 @@ var buildDeps = (config2 = {}) => {
|
|
|
26018
26085
|
const processRunner = config2.processRunner ?? defaultProcessRunner();
|
|
26019
26086
|
const logger = createWinstonLogger({ logLevel });
|
|
26020
26087
|
const makeAuth = config2.createAuth ?? createAuthManager;
|
|
26021
|
-
const auth = makeAuth({ cachePath, logger, fs, skipSystemBrowser: true, usePlaywrightFallback: true });
|
|
26088
|
+
const auth = makeAuth({ cachePath, logger, fs, skipSystemBrowser: true, usePlaywrightFallback: true, recaptureSecondaryViaBrowser: false });
|
|
26022
26089
|
const graph = createGraphClient(auth);
|
|
26023
26090
|
const makeLoginAuth = ({ useExtension }) => makeAuth({ cachePath, logger, fs, skipSystemBrowser: !useExtension, usePlaywrightFallback: !useExtension });
|
|
26024
26091
|
return { logger, auth, graph, processRunner, fs, makeLoginAuth };
|
package/dist/infra/auth.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ type SystemBrowserAuthFn = () => Promise<Result<{
|
|
|
81
81
|
type: string;
|
|
82
82
|
message: string;
|
|
83
83
|
}>>;
|
|
84
|
-
declare const createAuthManagerFromApi: (browserAuth: BrowserAuth, cachePath: string, browserProfileDir: string, logger: Logger, fs: FileSystem, systemBrowserAuthFn?: SystemBrowserAuthFn, usePlaywrightFallback?: boolean, skipSystemBrowser?: boolean) => AuthManager;
|
|
84
|
+
declare const createAuthManagerFromApi: (browserAuth: BrowserAuth, cachePath: string, browserProfileDir: string, logger: Logger, fs: FileSystem, systemBrowserAuthFn?: SystemBrowserAuthFn, usePlaywrightFallback?: boolean, skipSystemBrowser?: boolean, recaptureSecondaryViaBrowser?: boolean) => AuthManager;
|
|
85
85
|
/**
|
|
86
86
|
* QA-010: probe the token cache for a fresh access token. Handed to the
|
|
87
87
|
* browser capture so its poll loop can short-circuit the multi-minute dance
|
|
@@ -99,6 +99,7 @@ declare const createAuthManager: (deps: {
|
|
|
99
99
|
systemBrowserAuth?: SystemBrowserAuthFn;
|
|
100
100
|
usePlaywrightFallback?: boolean;
|
|
101
101
|
skipSystemBrowser?: boolean;
|
|
102
|
+
recaptureSecondaryViaBrowser?: boolean;
|
|
102
103
|
}) => AuthManager;
|
|
103
104
|
export { createAuthManager, createAuthManagerFromApi, createFreshCachedTokenProbe, stderrProgress };
|
|
104
105
|
export type { AuthError, AuthManager, ElevatedOutcome, SystemBrowserAuthFn };
|
package/docs/COMMANDS.md
CHANGED
|
@@ -223,7 +223,7 @@ For everything else:
|
|
|
223
223
|
|
|
224
224
|
| Command | Description | Required params | Graph endpoint |
|
|
225
225
|
|---------|-------------|-----------------|----------------|
|
|
226
|
-
| `find-chats-with-user` | Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical "all conversations with person X" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API. | `--name`, `--max-pages`, `--page-size` | `GET https://teams.microsoft.com/api/csa/{region}/api/v3/teams/users/me/chats` |
|
|
226
|
+
| `find-chats-with-user` | Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical "all conversations with person X" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). Cross-tenant resolution: the summary roster returns externally-homed counterparts as a bare object-id (no name/email), which a name search cannot match; for every bare DIRECT (1:1) chat the command hydrates the roster via the per-chat members endpoint and re-matches — so an external counterpart who is bare in your 1:1 is still found, even when they were already resolved in some meeting (the dual-identity case). Bare members in group/meeting chats are not deep-probed; when nothing matches and such members exist it returns a `hint` plus `unresolvedMemberCount` rather than a confident empty result. **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API. | `--name`, `--max-pages`, `--page-size` | `GET https://teams.microsoft.com/api/csa/{region}/api/v3/teams/users/me/chats` |
|
|
227
227
|
| `get-chat` | Return metadata for a single Microsoft Teams chat (1:1, group, or meeting). The CLI ships a slim default `--select=id,topic,chatType,createdDateTime,lastUpdatedDateTime`; pass `--select id,topic,webUrl,onlineMeetingInfo` (or any other comma-separated field list) to widen. Pass `--expand members` to inline membership. Returns metadata only — not the messages (which need `Chat.Read*`). Requires the M365ChatClient elevated token captured at login (the basic Teams web client token lacks `Chat.ReadBasic`). | `--chat-id`, `--select`, `--expand` | `GET /chats/{chat-id}` |
|
|
228
228
|
| `get-teams-chat-message` | Return a single Microsoft Teams chat message by its id via the chat substrate. Uses the chatsvcagg-audience bearer captured at login (same identity as the basic Teams token, different audience). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API. Source the chat-id + message-id via `list-teams-chats-with-messages` or `list-teams-chat-messages`. | `--chat-id`, `--message-id` | `GET https://teams.microsoft.com/api/csa/{region}/api/v1/chats/{chat-id}/messages/{message-id}` |
|
|
229
229
|
| `list-chat-members` | List the members of a single Microsoft Teams chat. Graph rejects `$top` / `$orderby` / `$expand` on this endpoint, so the CLI advertises only the subset Graph honours (`--skip`, `--select`, `--filter`). | `--chat-id`, `--skip`, `--select`, `--filter` | `GET /chats/{chat-id}/members` |
|
package/docs/commands.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "ask-marcel-office-cli",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"generatedAt": "2026-06-
|
|
3
|
+
"version": "1.5.2",
|
|
4
|
+
"generatedAt": "2026-06-23T11:35:14.255Z",
|
|
5
5
|
"commands": [
|
|
6
6
|
{
|
|
7
7
|
"name": "convert-calendar-event-attachment-to-markdown",
|
|
@@ -666,7 +666,7 @@
|
|
|
666
666
|
},
|
|
667
667
|
{
|
|
668
668
|
"name": "find-chats-with-user",
|
|
669
|
-
"summary": "Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical \"all conversations with person X\" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API.",
|
|
669
|
+
"summary": "Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical \"all conversations with person X\" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). Cross-tenant resolution: the summary roster returns externally-homed counterparts as a bare object-id (no name/email), which a name search cannot match; for every bare DIRECT (1:1) chat the command hydrates the roster via the per-chat members endpoint and re-matches — so an external counterpart who is bare in your 1:1 is still found, even when they were already resolved in some meeting (the dual-identity case). Bare members in group/meeting chats are not deep-probed; when nothing matches and such members exist it returns a `hint` plus `unresolvedMemberCount` rather than a confident empty result. **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API.",
|
|
670
670
|
"category": "chats",
|
|
671
671
|
"graphMethod": "GET",
|
|
672
672
|
"graphPathTemplate": "https://teams.microsoft.com/api/csa/{region}/api/v3/teams/users/me/chats",
|
|
@@ -682,7 +682,7 @@
|
|
|
682
682
|
"name": "max-pages",
|
|
683
683
|
"key": "maxPages",
|
|
684
684
|
"required": false,
|
|
685
|
-
"description": "Safety cap on the chat-list walk (positive integer; default
|
|
685
|
+
"description": "Safety cap on the chat-list walk (positive integer; default 20). The walk stops early once the substrate reports no more pages, so the cap only bites on accounts with more chats than `--max-pages × --page-size`; raise it (and watch `hasMore`) if a known chat is missed. Each page is one HTTP round-trip."
|
|
686
686
|
},
|
|
687
687
|
{
|
|
688
688
|
"name": "page-size",
|
|
@@ -692,7 +692,7 @@
|
|
|
692
692
|
}
|
|
693
693
|
],
|
|
694
694
|
"example": "ask-marcel find-chats-with-user --name 'Jane DOE'",
|
|
695
|
-
"responseShape": "`{ name, matches: [{ chatId, title, chatType, threadType, memberCount, lastMessageAt?, matchedMembers: [{ mri, displayName, email, userSubType }] }], matchCount, pagesFetched, chatsScanned, hasMore, nextContinuationToken? }`. `matchedMembers` always carries the matching entries' identifying fields — pass `chatId` into `list-teams-chat-history` to read message bodies. `hasMore: true` means `--max-pages` was hit before exhausting the chat list; chain with the existing `--continuation-token` flag on `list-teams-chats-with-messages` if you need to scan further (this command does not advertise a `--continuation-token` because resuming a partial search is rare; users either widen `--max-pages` or refine `--name`).",
|
|
695
|
+
"responseShape": "`{ name, matches: [{ chatId, title, chatType, threadType, memberCount, lastMessageAt?, matchedMembers: [{ mri, displayName, email, userSubType }] }], matchCount, pagesFetched, chatsScanned, chatsHydrated, unresolvedMemberCount, hasMore, nextContinuationToken?, hint? }`. `matchedMembers` always carries the matching entries' identifying fields — pass `chatId` into `list-teams-chat-history` to read message bodies. `chatsHydrated` counts the per-chat members lookups spent resolving bare cross-tenant members in direct (1:1) chats. `unresolvedMemberCount` is how many cross-tenant members are still unresolved by name (bare members in group/meeting chats, which are not deep-probed, plus any 1:1 hydration that errored); when `matchCount` is 0 and this is non-zero, a `hint` is present explaining the likely cause and the object-id / read-by-chat-id remedy — so an empty result is never silently confident. `hasMore: true` means `--max-pages` was hit before exhausting the chat list; chain with the existing `--continuation-token` flag on `list-teams-chats-with-messages` if you need to scan further (this command does not advertise a `--continuation-token` because resuming a partial search is rare; users either widen `--max-pages` or refine `--name`).",
|
|
696
696
|
"needsSubstrateToken": true,
|
|
697
697
|
"stability": "experimental"
|
|
698
698
|
},
|
|
@@ -2987,8 +2987,7 @@
|
|
|
2987
2987
|
],
|
|
2988
2988
|
"example": "ask-marcel list-chat-members --chat-id '19:abc...@thread.v2'",
|
|
2989
2989
|
"responseShape": "collection of Microsoft Graph `conversationMember` resources under `value[]`",
|
|
2990
|
-
"pagination": true
|
|
2991
|
-
"needsElevatedToken": true
|
|
2990
|
+
"pagination": true
|
|
2992
2991
|
},
|
|
2993
2992
|
{
|
|
2994
2993
|
"name": "list-chats",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ask-marcel-office-cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "Microsoft Graph CLI + library \u2014 typed Bun/TypeScript wrapper around 150+ Graph operations (read + on-the-fly PDF/markdown conversion + federated Microsoft Search) reachable from a Teams browser-OAuth token.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vincent Delacourt <vincent.delacourt@adama-development.com>",
|