ask-marcel-office-cli 1.5.1 → 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 +18 -0
- package/dist/cli.js +98 -41
- package/dist/commands.json +6 -7
- package/dist/index.js +97 -40
- package/docs/COMMANDS.md +1 -1
- package/docs/commands.json +6 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
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
|
+
|
|
5
23
|
## 1.5.1
|
|
6
24
|
|
|
7
25
|
### Fixed
|
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>",
|
|
@@ -3658,12 +3658,12 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
3658
3658
|
};
|
|
3659
3659
|
const recoverableElevatedFailureMessage = (reason) => {
|
|
3660
3660
|
if (reason === "launch_timeout") {
|
|
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,
|
|
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.)";
|
|
3662
3662
|
}
|
|
3663
3663
|
if (reason === "navigation_failed") {
|
|
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 /
|
|
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.";
|
|
3665
3665
|
}
|
|
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,
|
|
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.)";
|
|
3667
3667
|
};
|
|
3668
3668
|
const recaptureElevated = async () => {
|
|
3669
3669
|
try {
|
|
@@ -3699,7 +3699,7 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
3699
3699
|
return ok(validated.value);
|
|
3700
3700
|
}
|
|
3701
3701
|
if (!recaptureSecondaryViaBrowser)
|
|
3702
|
-
return err({ type: "auth_failed", message: failFastSecondaryMessage("Elevated (M365)", "list-chats, get-chat,
|
|
3702
|
+
return err({ type: "auth_failed", message: failFastSecondaryMessage("Elevated (M365)", "list-chats, get-chat, download-drive-item-version") });
|
|
3703
3703
|
return recaptureElevatedShared();
|
|
3704
3704
|
};
|
|
3705
3705
|
const freshChatsvcaggToken = (cached) => {
|
|
@@ -23014,7 +23014,7 @@ __export(exports_list_chat_members, {
|
|
|
23014
23014
|
});
|
|
23015
23015
|
var baseSchema21 = exports_external.object({ chatId: exports_external.string().min(1) });
|
|
23016
23016
|
var CHAT_MEMBERS_ODATA_KEYS = ["skip", "select", "filter"];
|
|
23017
|
-
var inner5 =
|
|
23017
|
+
var inner5 = buildPickODataListCommand((p) => `/chats/${p.chatId}/members`, baseSchema21, CHAT_MEMBERS_ODATA_KEYS);
|
|
23018
23018
|
var execute42 = async (graph, params) => {
|
|
23019
23019
|
const result = await inner5.execute(graph, params);
|
|
23020
23020
|
if (result.ok)
|
|
@@ -23049,8 +23049,7 @@ var meta44 = {
|
|
|
23049
23049
|
],
|
|
23050
23050
|
example: "ask-marcel list-chat-members --chat-id '19:abc...@thread.v2'",
|
|
23051
23051
|
responseShape: "collection of Microsoft Graph `conversationMember` resources under `value[]`",
|
|
23052
|
-
pagination: true
|
|
23053
|
-
needsElevatedToken: true
|
|
23052
|
+
pagination: true
|
|
23054
23053
|
};
|
|
23055
23054
|
|
|
23056
23055
|
// src/use-cases/commands/list-drive-item-permissions.ts
|
|
@@ -24012,8 +24011,13 @@ __export(exports_next_page, {
|
|
|
24012
24011
|
execute: () => execute74
|
|
24013
24012
|
});
|
|
24014
24013
|
var PREFIX = "https://graph.microsoft.com/v1.0";
|
|
24015
|
-
var
|
|
24016
|
-
|
|
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
|
+
};
|
|
24017
24021
|
var schema74 = exports_external.object({
|
|
24018
24022
|
url: exports_external.string().min(1).refine((v) => v.startsWith(`${PREFIX}/`), { message: `must be a Microsoft Graph v1.0 URL starting with ${PREFIX}/` })
|
|
24019
24023
|
});
|
|
@@ -26631,15 +26635,43 @@ var projectMember = (m) => {
|
|
|
26631
26635
|
out.userSubType = m.userSubType;
|
|
26632
26636
|
return out;
|
|
26633
26637
|
};
|
|
26634
|
-
var
|
|
26635
|
-
const
|
|
26636
|
-
|
|
26637
|
-
|
|
26638
|
-
|
|
26639
|
-
|
|
26640
|
-
|
|
26641
|
-
|
|
26642
|
-
|
|
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: [] };
|
|
26643
26675
|
let continuationToken;
|
|
26644
26676
|
let pagesFetched = 0;
|
|
26645
26677
|
let chatsScanned = 0;
|
|
@@ -26654,42 +26686,67 @@ var execute107 = async (graph, params) => {
|
|
|
26654
26686
|
const chats = body.chats ?? [];
|
|
26655
26687
|
pagesFetched += 1;
|
|
26656
26688
|
chatsScanned += chats.length;
|
|
26657
|
-
for (const chat of chats)
|
|
26658
|
-
|
|
26659
|
-
continue;
|
|
26660
|
-
const members = chat.members ?? [];
|
|
26661
|
-
const matchedMembers = members.filter((m) => memberMatches(m, queryFolded));
|
|
26662
|
-
if (matchedMembers.length === 0)
|
|
26663
|
-
continue;
|
|
26664
|
-
seenChatIds.add(chat.id);
|
|
26665
|
-
matched.push({
|
|
26666
|
-
chatId: chat.id,
|
|
26667
|
-
title: chat.title ?? null,
|
|
26668
|
-
chatType: chat.chatType,
|
|
26669
|
-
threadType: chat.threadType,
|
|
26670
|
-
memberCount: members.length,
|
|
26671
|
-
...chat.lastMessage?.composeTime !== undefined ? { lastMessageAt: chat.lastMessage.composeTime } : {},
|
|
26672
|
-
matchedMembers: matchedMembers.map(projectMember)
|
|
26673
|
-
});
|
|
26674
|
-
}
|
|
26689
|
+
for (const chat of chats)
|
|
26690
|
+
collectChat(chat, queryFolded, acc);
|
|
26675
26691
|
if (body.hasMoreData !== true || body.continuationToken === undefined) {
|
|
26676
26692
|
continuationToken = undefined;
|
|
26677
26693
|
break;
|
|
26678
26694
|
}
|
|
26679
26695
|
continuationToken = body.continuationToken;
|
|
26680
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);
|
|
26681
26735
|
return ok({
|
|
26682
26736
|
name: parsed.data.name,
|
|
26683
26737
|
matches: matched,
|
|
26684
26738
|
matchCount: matched.length,
|
|
26685
26739
|
pagesFetched,
|
|
26686
26740
|
chatsScanned,
|
|
26741
|
+
chatsHydrated,
|
|
26742
|
+
unresolvedMemberCount,
|
|
26687
26743
|
hasMore: continuationToken !== undefined,
|
|
26688
|
-
nextContinuationToken: continuationToken
|
|
26744
|
+
nextContinuationToken: continuationToken,
|
|
26745
|
+
...matched.length === 0 && unresolvedMemberCount > 0 ? { hint: HINT2 } : {}
|
|
26689
26746
|
});
|
|
26690
26747
|
};
|
|
26691
26748
|
var meta109 = {
|
|
26692
|
-
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.',
|
|
26693
26750
|
category: "chats",
|
|
26694
26751
|
needsSubstrateToken: true,
|
|
26695
26752
|
graphMethod: "GET",
|
|
@@ -26706,7 +26763,7 @@ var meta109 = {
|
|
|
26706
26763
|
name: "max-pages",
|
|
26707
26764
|
key: "maxPages",
|
|
26708
26765
|
required: false,
|
|
26709
|
-
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."
|
|
26710
26767
|
},
|
|
26711
26768
|
{
|
|
26712
26769
|
name: "page-size",
|
|
@@ -26716,7 +26773,7 @@ var meta109 = {
|
|
|
26716
26773
|
}
|
|
26717
26774
|
],
|
|
26718
26775
|
example: "ask-marcel find-chats-with-user --name 'Jane DOE'",
|
|
26719
|
-
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`).",
|
|
26720
26777
|
stability: "experimental"
|
|
26721
26778
|
};
|
|
26722
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
|
@@ -1432,12 +1432,12 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
1432
1432
|
};
|
|
1433
1433
|
const recoverableElevatedFailureMessage = (reason) => {
|
|
1434
1434
|
if (reason === "launch_timeout") {
|
|
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,
|
|
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.)";
|
|
1436
1436
|
}
|
|
1437
1437
|
if (reason === "navigation_failed") {
|
|
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 /
|
|
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.";
|
|
1439
1439
|
}
|
|
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,
|
|
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.)";
|
|
1441
1441
|
};
|
|
1442
1442
|
const recaptureElevated = async () => {
|
|
1443
1443
|
try {
|
|
@@ -1473,7 +1473,7 @@ var createAuthManagerFromApi = (browserAuth, cachePath, browserProfileDir, logge
|
|
|
1473
1473
|
return ok(validated.value);
|
|
1474
1474
|
}
|
|
1475
1475
|
if (!recaptureSecondaryViaBrowser)
|
|
1476
|
-
return err({ type: "auth_failed", message: failFastSecondaryMessage("Elevated (M365)", "list-chats, get-chat,
|
|
1476
|
+
return err({ type: "auth_failed", message: failFastSecondaryMessage("Elevated (M365)", "list-chats, get-chat, download-drive-item-version") });
|
|
1477
1477
|
return recaptureElevatedShared();
|
|
1478
1478
|
};
|
|
1479
1479
|
const freshChatsvcaggToken = (cached) => {
|
|
@@ -19906,7 +19906,7 @@ __export(exports_list_chat_members, {
|
|
|
19906
19906
|
});
|
|
19907
19907
|
var baseSchema21 = exports_external.object({ chatId: exports_external.string().min(1) });
|
|
19908
19908
|
var CHAT_MEMBERS_ODATA_KEYS = ["skip", "select", "filter"];
|
|
19909
|
-
var inner5 =
|
|
19909
|
+
var inner5 = buildPickODataListCommand((p) => `/chats/${p.chatId}/members`, baseSchema21, CHAT_MEMBERS_ODATA_KEYS);
|
|
19910
19910
|
var execute42 = async (graph, params) => {
|
|
19911
19911
|
const result = await inner5.execute(graph, params);
|
|
19912
19912
|
if (result.ok)
|
|
@@ -19941,8 +19941,7 @@ var meta44 = {
|
|
|
19941
19941
|
],
|
|
19942
19942
|
example: "ask-marcel list-chat-members --chat-id '19:abc...@thread.v2'",
|
|
19943
19943
|
responseShape: "collection of Microsoft Graph `conversationMember` resources under `value[]`",
|
|
19944
|
-
pagination: true
|
|
19945
|
-
needsElevatedToken: true
|
|
19944
|
+
pagination: true
|
|
19946
19945
|
};
|
|
19947
19946
|
|
|
19948
19947
|
// src/use-cases/commands/list-drive-item-permissions.ts
|
|
@@ -20904,8 +20903,13 @@ __export(exports_next_page, {
|
|
|
20904
20903
|
execute: () => execute74
|
|
20905
20904
|
});
|
|
20906
20905
|
var PREFIX = "https://graph.microsoft.com/v1.0";
|
|
20907
|
-
var
|
|
20908
|
-
|
|
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
|
+
};
|
|
20909
20913
|
var schema74 = exports_external.object({
|
|
20910
20914
|
url: exports_external.string().min(1).refine((v) => v.startsWith(`${PREFIX}/`), { message: `must be a Microsoft Graph v1.0 URL starting with ${PREFIX}/` })
|
|
20911
20915
|
});
|
|
@@ -23523,15 +23527,43 @@ var projectMember = (m) => {
|
|
|
23523
23527
|
out.userSubType = m.userSubType;
|
|
23524
23528
|
return out;
|
|
23525
23529
|
};
|
|
23526
|
-
var
|
|
23527
|
-
const
|
|
23528
|
-
|
|
23529
|
-
|
|
23530
|
-
|
|
23531
|
-
|
|
23532
|
-
|
|
23533
|
-
|
|
23534
|
-
|
|
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: [] };
|
|
23535
23567
|
let continuationToken;
|
|
23536
23568
|
let pagesFetched = 0;
|
|
23537
23569
|
let chatsScanned = 0;
|
|
@@ -23546,42 +23578,67 @@ var execute107 = async (graph, params) => {
|
|
|
23546
23578
|
const chats = body.chats ?? [];
|
|
23547
23579
|
pagesFetched += 1;
|
|
23548
23580
|
chatsScanned += chats.length;
|
|
23549
|
-
for (const chat of chats)
|
|
23550
|
-
|
|
23551
|
-
continue;
|
|
23552
|
-
const members = chat.members ?? [];
|
|
23553
|
-
const matchedMembers = members.filter((m) => memberMatches(m, queryFolded));
|
|
23554
|
-
if (matchedMembers.length === 0)
|
|
23555
|
-
continue;
|
|
23556
|
-
seenChatIds.add(chat.id);
|
|
23557
|
-
matched.push({
|
|
23558
|
-
chatId: chat.id,
|
|
23559
|
-
title: chat.title ?? null,
|
|
23560
|
-
chatType: chat.chatType,
|
|
23561
|
-
threadType: chat.threadType,
|
|
23562
|
-
memberCount: members.length,
|
|
23563
|
-
...chat.lastMessage?.composeTime !== undefined ? { lastMessageAt: chat.lastMessage.composeTime } : {},
|
|
23564
|
-
matchedMembers: matchedMembers.map(projectMember)
|
|
23565
|
-
});
|
|
23566
|
-
}
|
|
23581
|
+
for (const chat of chats)
|
|
23582
|
+
collectChat(chat, queryFolded, acc);
|
|
23567
23583
|
if (body.hasMoreData !== true || body.continuationToken === undefined) {
|
|
23568
23584
|
continuationToken = undefined;
|
|
23569
23585
|
break;
|
|
23570
23586
|
}
|
|
23571
23587
|
continuationToken = body.continuationToken;
|
|
23572
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);
|
|
23573
23627
|
return ok({
|
|
23574
23628
|
name: parsed.data.name,
|
|
23575
23629
|
matches: matched,
|
|
23576
23630
|
matchCount: matched.length,
|
|
23577
23631
|
pagesFetched,
|
|
23578
23632
|
chatsScanned,
|
|
23633
|
+
chatsHydrated,
|
|
23634
|
+
unresolvedMemberCount,
|
|
23579
23635
|
hasMore: continuationToken !== undefined,
|
|
23580
|
-
nextContinuationToken: continuationToken
|
|
23636
|
+
nextContinuationToken: continuationToken,
|
|
23637
|
+
...matched.length === 0 && unresolvedMemberCount > 0 ? { hint: HINT2 } : {}
|
|
23581
23638
|
});
|
|
23582
23639
|
};
|
|
23583
23640
|
var meta109 = {
|
|
23584
|
-
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.',
|
|
23585
23642
|
category: "chats",
|
|
23586
23643
|
needsSubstrateToken: true,
|
|
23587
23644
|
graphMethod: "GET",
|
|
@@ -23598,7 +23655,7 @@ var meta109 = {
|
|
|
23598
23655
|
name: "max-pages",
|
|
23599
23656
|
key: "maxPages",
|
|
23600
23657
|
required: false,
|
|
23601
|
-
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."
|
|
23602
23659
|
},
|
|
23603
23660
|
{
|
|
23604
23661
|
name: "page-size",
|
|
@@ -23608,7 +23665,7 @@ var meta109 = {
|
|
|
23608
23665
|
}
|
|
23609
23666
|
],
|
|
23610
23667
|
example: "ask-marcel find-chats-with-user --name 'Jane DOE'",
|
|
23611
|
-
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`).",
|
|
23612
23669
|
stability: "experimental"
|
|
23613
23670
|
};
|
|
23614
23671
|
|
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>",
|