ask-marcel-office-cli 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/dist/cli.js +254 -348
- package/dist/commands.json +70 -717
- package/dist/composition/run-registry-command.d.ts +12 -3
- package/dist/index.js +203 -266
- package/dist/presenter/output.d.ts +2 -2
- package/dist/presenter/render-to-string.d.ts +34 -2
- package/dist/use-cases/commands/command-types.d.ts +1 -20
- package/dist/use-cases/commands/create-forward-draft.d.ts +1 -1
- package/dist/use-cases/commands/create-reply-draft.d.ts +1 -1
- package/dist/use-cases/commands/docs-render.d.ts +0 -1
- package/dist/use-cases/commands/draft-comment-splicer.d.ts +8 -1
- package/dist/use-cases/commands/list-mail-folder-messages-delta.d.ts +10 -2
- package/dist/use-cases/commands/reject-unknown-params.d.ts +16 -0
- package/dist/use-cases/commands/resolve-command.d.ts +7 -9
- package/dist/use-cases/commands/search-onenote-pages.d.ts +1 -1
- package/dist/use-cases/commands/update-mail-draft.d.ts +4 -0
- package/docs/COMMANDS.md +7 -7
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Logger } from '../use-cases/ports/logger.js';
|
|
2
2
|
import type { ErrorSource } from './error-hints.js';
|
|
3
|
-
import type { OutputFormat } from './render-to-string.js';
|
|
4
|
-
declare const render: (data: unknown, logger: Logger, format: OutputFormat) => void;
|
|
3
|
+
import type { OutputFormat, SizeHintContext } from './render-to-string.js';
|
|
4
|
+
declare const render: (data: unknown, logger: Logger, format: OutputFormat, context?: SizeHintContext) => void;
|
|
5
5
|
declare const renderError: (message: string, format: OutputFormat, errorCode?: string, explicitSource?: ErrorSource, retryAfterSeconds?: number) => void;
|
|
6
6
|
export { render, renderError };
|
|
7
7
|
export type { OutputFormat };
|
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
import type { ErrorSource } from './error-hints.js';
|
|
2
2
|
type OutputFormat = 'text' | 'json';
|
|
3
|
+
/**
|
|
4
|
+
* Which front end the caller is on. It changes what a remedy can even BE: a
|
|
5
|
+
* terminal caller can redirect stdout to a file, an MCP client cannot (it has
|
|
6
|
+
* no shell), and the `--output-path` flag reaches MCP as an `outputPath` tool
|
|
7
|
+
* param rather than a flag.
|
|
8
|
+
*/
|
|
9
|
+
type RenderSurface = 'cli' | 'mcp';
|
|
10
|
+
/**
|
|
11
|
+
* What the presenter needs in order to name a remedy the caller can actually
|
|
12
|
+
* use. Built in composition (which owns the command manifest); omitted by the
|
|
13
|
+
* renders that have no command behind them at all — the 548 KB `list-commands`
|
|
14
|
+
* manifest, a login summary, a logout status.
|
|
15
|
+
*
|
|
16
|
+
* 2026-07-23: the banner used to call `--output-path` a "universal remedy
|
|
17
|
+
* (works on every command)". It is not. Plain-JSON commands REFUSE the flag,
|
|
18
|
+
* and the two commands that trip the banner most often (`search-all-files`,
|
|
19
|
+
* `microsoft-search-query`) advertise only `--query` — so every remedy the
|
|
20
|
+
* banner named was a dead end there, and an agentic caller following it burned
|
|
21
|
+
* a call to find out.
|
|
22
|
+
*/
|
|
23
|
+
type SizeHintContext = {
|
|
24
|
+
readonly commandName: string;
|
|
25
|
+
/** `meta.producesBytes` — the manifest flag that decides whether --output-path is accepted. */
|
|
26
|
+
readonly producesBytes: boolean;
|
|
27
|
+
readonly supportsSelect: boolean;
|
|
28
|
+
readonly supportsTop: boolean;
|
|
29
|
+
readonly surface: RenderSurface;
|
|
30
|
+
};
|
|
3
31
|
/**
|
|
4
32
|
* Render a use-case success value to its final string, newline included.
|
|
5
33
|
* `output.ts` writes this to stdout; `mcp.ts` returns it as tool content.
|
|
34
|
+
*
|
|
35
|
+
* `context` is what makes the oversized-response banner honest — pass it
|
|
36
|
+
* whenever a registry command produced the data. Omit it for the renders that
|
|
37
|
+
* have no command behind them; the banner then claims no flag-level remedy.
|
|
6
38
|
*/
|
|
7
|
-
declare const renderToString: (data: unknown, format: OutputFormat) => string;
|
|
39
|
+
declare const renderToString: (data: unknown, format: OutputFormat, context?: SizeHintContext) => string;
|
|
8
40
|
/**
|
|
9
41
|
* Render an error to its final string, newline included. The `hint` / `source`
|
|
10
42
|
* lookup is shared with the CLI so an MCP consumer sees the same curated
|
|
@@ -12,4 +44,4 @@ declare const renderToString: (data: unknown, format: OutputFormat) => string;
|
|
|
12
44
|
*/
|
|
13
45
|
declare const renderErrorToString: (message: string, format: OutputFormat, errorCode?: string, explicitSource?: ErrorSource, retryAfterSeconds?: number) => string;
|
|
14
46
|
export { renderErrorToString, renderToString };
|
|
15
|
-
export type { OutputFormat };
|
|
47
|
+
export type { OutputFormat, RenderSurface, SizeHintContext };
|
|
@@ -5,10 +5,6 @@ type CommandSchema = z.ZodType;
|
|
|
5
5
|
type CommandExecute = (graph: GraphClient, params: Record<string, string>) => Promise<Result<unknown, import('../../infra/graph-client.js').GraphError>>;
|
|
6
6
|
type CommandCategory = 'drive' | 'excel' | 'sharepoint' | 'tasks' | 'mail' | 'notes' | 'user' | 'calendar' | 'chats' | 'teams' | 'meta' | 'lifecycle';
|
|
7
7
|
type CommandHttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
8
|
-
type CommandOptionAlias = {
|
|
9
|
-
readonly name: string;
|
|
10
|
-
readonly key: string;
|
|
11
|
-
};
|
|
12
8
|
/**
|
|
13
9
|
* Structured type-hint for a CLI flag value. Surfaces in `help-json` so an
|
|
14
10
|
* LLM can avoid the trial-and-error of "is this an ID or a name?" prose
|
|
@@ -38,13 +34,6 @@ type CommandOptionMeta = {
|
|
|
38
34
|
* commands accept but do not demand.
|
|
39
35
|
*/
|
|
40
36
|
readonly required: boolean;
|
|
41
|
-
/**
|
|
42
|
-
* Optional secondary spellings of the same flag. Both the canonical
|
|
43
|
-
* `name` and every alias name are accepted on the command line; values
|
|
44
|
-
* passed under an alias are normalized to the canonical `key` before
|
|
45
|
-
* the schema runs. The canonical name is what `--help` shows first.
|
|
46
|
-
*/
|
|
47
|
-
readonly aliases?: ReadonlyArray<CommandOptionAlias>;
|
|
48
37
|
/**
|
|
49
38
|
* Structured value-type hint for LLM consumers. Optional.
|
|
50
39
|
*/
|
|
@@ -80,14 +69,6 @@ type PaginationStrategy =
|
|
|
80
69
|
type CommandMeta = {
|
|
81
70
|
readonly summary: string;
|
|
82
71
|
readonly category: CommandCategory;
|
|
83
|
-
/**
|
|
84
|
-
* Deprecated former command names kept working as commander-level aliases for
|
|
85
|
-
* back-compat after a rename (e.g. `download-onedrive-file-content` →
|
|
86
|
-
* `download-drive-item-content`). The canonical registry key is what `--help`
|
|
87
|
-
* and the manifest list first; each alias here is also accepted on the CLI and
|
|
88
|
-
* surfaced in the manifest so an LLM that learned the old name still resolves.
|
|
89
|
-
*/
|
|
90
|
-
readonly commandAliases?: ReadonlyArray<string>;
|
|
91
72
|
readonly graphMethod: CommandHttpMethod;
|
|
92
73
|
readonly graphPathTemplate: string;
|
|
93
74
|
readonly graphDocsUrl: string;
|
|
@@ -178,4 +159,4 @@ type Command = {
|
|
|
178
159
|
*/
|
|
179
160
|
readonly executeLocal?: (fs: import('../ports/filesystem.js').FileSystem, params: Record<string, string>) => Promise<Result<unknown, import('../../infra/graph-client.js').GraphError>>;
|
|
180
161
|
};
|
|
181
|
-
export type { ArgumentHint, Command, CommandCategory, CommandExecute, CommandHttpMethod, CommandMeta,
|
|
162
|
+
export type { ArgumentHint, Command, CommandCategory, CommandExecute, CommandHttpMethod, CommandMeta, CommandOptionMeta, CommandPositionalArgumentMeta, CommandSchema, PaginationStrategy, };
|
|
@@ -4,7 +4,7 @@ declare const schema: z.ZodObject<{
|
|
|
4
4
|
forwardMessageId: z.ZodString;
|
|
5
5
|
toRecipients: z.ZodString;
|
|
6
6
|
ccRecipients: z.ZodOptional<z.ZodString>;
|
|
7
|
-
|
|
7
|
+
comment: z.ZodString;
|
|
8
8
|
subject: z.ZodOptional<z.ZodString>;
|
|
9
9
|
bodyContentType: z.ZodOptional<z.ZodEnum<{
|
|
10
10
|
Text: "Text";
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import type { Command, CommandMeta } from './command-types.js';
|
|
3
3
|
declare const schema: z.ZodObject<{
|
|
4
4
|
replyToMessageId: z.ZodString;
|
|
5
|
-
|
|
5
|
+
comment: z.ZodString;
|
|
6
6
|
subject: z.ZodOptional<z.ZodString>;
|
|
7
7
|
replyAll: z.ZodOptional<z.ZodEnum<{
|
|
8
8
|
true: "true";
|
|
@@ -3,7 +3,6 @@ export type CommandManifestEntry = {
|
|
|
3
3
|
readonly name: string;
|
|
4
4
|
readonly summary: string;
|
|
5
5
|
readonly category: CommandCategory;
|
|
6
|
-
readonly commandAliases?: CommandMeta['commandAliases'];
|
|
7
6
|
readonly graphMethod: CommandMeta['graphMethod'];
|
|
8
7
|
readonly graphPathTemplate: string;
|
|
9
8
|
readonly graphDocsUrl: string;
|
|
@@ -21,9 +21,16 @@ declare const findBodyInsertStart: (html: string) => number;
|
|
|
21
21
|
* silently dropping the real quoted history below.
|
|
22
22
|
*/
|
|
23
23
|
declare const commentCarriesQuoteBoundary: (commentHtml: string) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* True when a draft body still carries the quoted history Graph minted with it.
|
|
26
|
+
* Dispatches on the draft's own contentType: the HTML and plain-text boundary
|
|
27
|
+
* markers are different things entirely. Used to refuse a whole-body replace
|
|
28
|
+
* that would drop the quote (2026-07-23 bug report).
|
|
29
|
+
*/
|
|
30
|
+
declare const bodyCarriesQuote: (contentType: string, content: string) => boolean;
|
|
24
31
|
/** The refusal copy for `commentCarriesQuoteBoundary`, named for the flag that carried it. */
|
|
25
32
|
declare const boundaryMarkerRefusal: (flagName: string) => string;
|
|
26
33
|
declare const insertCommentAboveQuote: (html: string, commentHtml: string) => SpliceResult;
|
|
27
34
|
declare const replaceCommentAboveQuote: (html: string, commentHtml: string) => SpliceResult;
|
|
28
35
|
declare const replacePlainTextCommentAboveQuote: (text: string, comment: string) => PlainTextSpliceResult;
|
|
29
|
-
export { boundaryMarkerRefusal, commentCarriesQuoteBoundary, escapeTextAsHtml, findBodyInsertStart, insertCommentAboveQuote, replaceCommentAboveQuote, replacePlainTextCommentAboveQuote, };
|
|
36
|
+
export { bodyCarriesQuote, boundaryMarkerRefusal, commentCarriesQuoteBoundary, escapeTextAsHtml, findBodyInsertStart, insertCommentAboveQuote, replaceCommentAboveQuote, replacePlainTextCommentAboveQuote, };
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Command, CommandMeta } from './command-types.js';
|
|
3
|
+
declare const schema: z.ZodObject<{
|
|
4
|
+
mailFolderId: z.ZodString;
|
|
5
|
+
filter: z.ZodOptional<z.ZodString>;
|
|
6
|
+
select: z.ZodOptional<z.ZodString>;
|
|
7
|
+
top: z.ZodOptional<z.ZodString>;
|
|
8
|
+
expand: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
declare const execute: Command['execute'];
|
|
3
11
|
declare const meta: CommandMeta;
|
|
4
12
|
export { execute, meta, schema };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Result } from '../../domain/result.js';
|
|
2
|
+
import type { GraphError } from '../../infra/graph-client.js';
|
|
3
|
+
import type { Command } from './command-types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Returns the rejection for any param key the command does not declare, or
|
|
6
|
+
* `undefined` when every key is known. Exported for the registry wrapper.
|
|
7
|
+
*/
|
|
8
|
+
declare const rejectUnknownParams: (schema: Command["schema"], params: Record<string, string>) => Result<never, GraphError> | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Wraps a command so `execute` / `executeLocal` refuse unknown params before
|
|
11
|
+
* running. Applied at registry assembly, which is the single choke point every
|
|
12
|
+
* surface passes through — including a library caller that never touches
|
|
13
|
+
* composition.
|
|
14
|
+
*/
|
|
15
|
+
declare const withUnknownParamRejection: (command: Command) => Command;
|
|
16
|
+
export { rejectUnknownParams, withUnknownParamRejection };
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import type { Result } from '../../domain/result.js';
|
|
2
2
|
import type { Command } from './command-types.js';
|
|
3
3
|
/**
|
|
4
|
-
* Name -> command lookup
|
|
4
|
+
* Name -> command lookup shared by every non-commander front end.
|
|
5
5
|
*
|
|
6
|
-
* The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* (`get-command-docs`, `run-command`, `run-write-command`) each need the same
|
|
10
|
-
* lookup — Rule of Three, so it lives here rather than inline.
|
|
6
|
+
* The MCP gateway's three command-taking tools (`get-command-docs`,
|
|
7
|
+
* `run-command`, `run-write-command`) each need the same lookup — Rule of
|
|
8
|
+
* Three, so it lives here rather than inline.
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
* (
|
|
14
|
-
*
|
|
10
|
+
* 2026-07-24: one name per command. Deprecated-name resolution
|
|
11
|
+
* (`meta.commandAliases`) was removed with the alias system; an old name gets
|
|
12
|
+
* the unknown-command rejection with the full list, same as any typo.
|
|
15
13
|
*/
|
|
16
14
|
export type ResolveCommandError = {
|
|
17
15
|
readonly type: 'unknown_command';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { Command, CommandMeta } from './command-types.js';
|
|
3
3
|
declare const schema: z.ZodObject<{
|
|
4
|
-
|
|
4
|
+
query: z.ZodString;
|
|
5
5
|
select: z.ZodOptional<z.ZodString>;
|
|
6
6
|
top: z.ZodOptional<z.ZodString>;
|
|
7
7
|
skip: z.ZodOptional<z.ZodString>;
|
|
@@ -5,6 +5,10 @@ declare const schema: z.ZodObject<{
|
|
|
5
5
|
subject: z.ZodOptional<z.ZodString>;
|
|
6
6
|
bodyContent: z.ZodOptional<z.ZodString>;
|
|
7
7
|
comment: z.ZodOptional<z.ZodString>;
|
|
8
|
+
replaceQuotedHistory: z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
true: "true";
|
|
10
|
+
false: "false";
|
|
11
|
+
}>>;
|
|
8
12
|
bodyContentType: z.ZodOptional<z.ZodEnum<{
|
|
9
13
|
Text: "Text";
|
|
10
14
|
HTML: "HTML";
|
package/docs/COMMANDS.md
CHANGED
|
@@ -36,12 +36,12 @@ For everything else:
|
|
|
36
36
|
| `download-drive-item-version` | Download a *non-current* historical version of a OneDrive / SharePoint file. `--format original` (default) returns the raw bytes — Graph refuses to serve the current version through this endpoint with "You cannot get the content of the current version"; for the current version use `download-drive-item-content`. `--format pdf` runs Graph `?format=pdf` for Office docs; plain-text and `pdf` sources short-circuit to raw bytes with `passthrough: true` + a note (Graph rejects `pdf → pdf` with InputFormatNotSupported). `--format markdown` runs the local conversion pipeline (mammoth for docx, sheetjs for xlsx, csv → table, odt/ods/odp via content.xml, plain-text passthrough). All three formats use an M365ChatClient-elevated Graph token (captured at login from m365.cloud.microsoft) — the Teams web client token returns 403 logicalPermissionAccessDenied on historical-version stream content. The CLI follows the SharePoint streamContent redirect internally so the LLM never has to fetch an external URL. caveat for `--format pdf`: Graph sometimes silently falls back to raw source bytes for the current version (which Graph occasionally serves through this endpoint) — when the response carries `passthrough: true`, save with the source extension, not `.pdf` (the global output-path flag refuses the mismatch). | `--drive-id`, `--item-id`, `--version-id`, `--format`, `--include-metadata` | `GET /drives/{drive-id}/items/{item-id}/versions/{version-id}/content` |
|
|
37
37
|
| `extract-drive-item-images` | Extract the embedded images from a OneDrive / SharePoint document. For docx / xlsx / pptx (and their macro-enabled / template variants) it reads the OOXML media parts directly (png/jpg/gif/bmp/tiff/webp/svg) — including original full-resolution / un-cropped originals and images on hidden slides the rendered view never shows. For a pdf it walks every page via unpdf and re-encodes each painted image as PNG (note: page-oriented — it captures images as painted on each page, but NOT layer-hidden/unpainted XObjects or the full uncropped original behind a clipped image). Pair with the global output-dir flag to write every image to a folder; otherwise the bytes ride back base64-encoded in the response. svg rides back as its XML source (which carries the diagram text labels); legacy vector (emf/wmf) and audio/video are skipped. For any other format the command returns a 415 pointing at `download-drive-item-content`. | `--drive-id`, `--item-id`, `--tenant-id` | `GET /drives/{drive-id}/items/{item-id}/content` |
|
|
38
38
|
| `extract-sharepoint-links-in-documents` | Find every `*.sharepoint.com` URL embedded in a Word / Excel / PowerPoint or OpenDocument file on OneDrive or SharePoint and resolve each one to its driveItem (driveId, itemId, name, webUrl) so the agent can feed those into `download-drive-item-as-pdf` / `-as-markdown` etc. The document sibling of `extract-sharepoint-links-in-mail`. For OOXML (.docx/.xlsx/.pptx) it reads external hyperlinks from the package’s relationship parts (`_rels/*.rels`, `TargetMode="External"`); for OpenDocument (.odt/.ods/.odp) it reads the inline `xlink:href` links in content.xml / styles.xml — either way it catches links wherever they live (body text, headers/footers, cell formulas, slide shapes). Read-only — no conversion happens here. Capped at 25 unique URLs per call (returns `truncated: true` and `skippedCount` when there are more); duplicates are deduplicated; per-link errors are captured inside each entry instead of failing the whole call. Non-zip inputs (pdf/images) return an api_error. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content` |
|
|
39
|
-
| `get-drive-delta` | Get the incremental change set (added / modified / deleted items) under a OneDrive / SharePoint folder. Use the `@odata.deltaLink` from a previous response to resume. | `--drive-id`, `--item-id`, `--top`, `--select`, `--
|
|
39
|
+
| `get-drive-delta` | Get the incremental change set (added / modified / deleted items) under a OneDrive / SharePoint folder. Use the `@odata.deltaLink` from a previous response to resume. | `--drive-id`, `--item-id`, `--top`, `--select`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/delta()` |
|
|
40
40
|
| `get-drive-item` | Get the metadata (driveItem resource) of a single file or folder in OneDrive / SharePoint. Use `--select` to slim the response — a full driveItem can run >10 KB with all the optional facets. | `--drive-id`, `--item-id`, `--tenant-id`, `--select`, `--expand` | `GET /drives/{drive-id}/items/{item-id}` |
|
|
41
41
|
| `get-drive-item-analytics` | Return view / activity analytics for a OneDrive / SharePoint file — `allTime` totals (views, viewers) and `lastSevenDays` rollup. Useful for ranking files by attention or detecting stale content. **Known empty case**: returns `{ allTime: null, lastSevenDays: null }` on low-traffic items, or when the calling identity (the Teams web client basic token) lacks the analytics scope on the tenant. Do not interpret nulls as "no views" — interpret as "not available for this caller". For active files where you expect data and see nulls, escalate to a token with `Reports.Read.All`. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/analytics` |
|
|
42
42
|
| `get-drive-item-created-by-user` | Return the `user` resource for whoever created a OneDrive / SharePoint file — full profile, not just the truncated `createdBy.user` summary embedded in the parent driveItem. Useful when you need title / department / mail of the author. Use `--select` to fetch only the fields you care about (e.g. `--select id,displayName,jobTitle,department,mail`). | `--drive-id`, `--item-id`, `--select`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/createdByUser` |
|
|
43
43
|
| `get-drive-item-last-modified-by-user` | Return the full `user` resource for whoever last modified a OneDrive / SharePoint file — sibling to `get-drive-item-created-by-user`. Use `--select` to fetch only specific fields. | `--drive-id`, `--item-id`, `--select`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/lastModifiedByUser` |
|
|
44
|
-
| `get-drive-root-delta` | Track incremental changes (added / modified / deleted items) anywhere under the signed-in user's OneDrive root. **Takes zero required arguments** — acts implicitly on the signed-in user's primary OneDrive; use `get-drive-delta` to target a specific drive by ID. The first call returns a snapshot plus `@odata.deltaLink`; subsequent calls with that link return only what has changed since. Cross-folder companion to `get-drive-delta` (which scopes to one specific folder). | `--top`, `--select`, `--
|
|
44
|
+
| `get-drive-root-delta` | Track incremental changes (added / modified / deleted items) anywhere under the signed-in user's OneDrive root. **Takes zero required arguments** — acts implicitly on the signed-in user's primary OneDrive; use `get-drive-delta` to target a specific drive by ID. The first call returns a snapshot plus `@odata.deltaLink`; subsequent calls with that link return only what has changed since. Cross-folder companion to `get-drive-delta` (which scopes to one specific folder). | `--top`, `--select`, `--expand` | `GET /me/drive/root/delta()` |
|
|
45
45
|
| `get-drive-root-item` | Get the root folder (driveItem) of a OneDrive / SharePoint drive. Use `--select` to slim the response (e.g. `--select id,name,folder`). | `--drive-id`, `--select`, `--expand` | `GET /drives/{drive-id}/root` |
|
|
46
46
|
| `get-drive-special-folder` | Resolve a OneDrive well-known folder via `--folder-name` (one of `documents`, `photos`, `cameraroll`, `approot`, `music`, `attachments`) without having to navigate from the root. Returns the folder's driveItem (id, name, parentReference, etc.) ready to feed into `list-folder-files` or `download-drive-item-content`. | `--folder-name`, `--select`, `--expand` | `GET /me/drive/special/{folder-name}` |
|
|
47
47
|
| `list-accessible-drives` | Enumerate every drive (document library) the signed-in user can reach — personal OneDrive(s), Teams libraries, SharePoint M365-group sites, drives behind files shared with the user, private/shared Teams channel sites, drives behind recently-used / followed / trending items (activity signals), AND every NON-default document library of each discovered SharePoint site — by unioning `/me/drives`, `/me/joinedTeams`, `/me/memberOf` (Unified groups → `/groups/{id}/drive`), `/me/drive/sharedWithMe`, per-team `/teams/{id}/channels` → `/channels/{ch}/filesFolder` (private/shared channels only — their files live in their own site, not the team default drive), `/me/drive/recent` + `/me/drive/following` + `/me/insights/{trending,used,shared}`, and a path-addressed `/sites/{host}:/sites/{name}:/drives` per discovered site (catches secondary libraries like "Teams Wiki Data" the default-drive vectors miss). Unlike `search-sharepoint-sites-by-name` (which relies on the tenant search index and misses direct-link-only sites + OneDrives), these vectors surface drives the search index never returns; the index in turn returns sites you can open but are not a member of, so the *union of both commands* is the practical maximum on a delegated token. Each drive is tagged with the `sources[]` that found it (a drive can have several). Per-resource "can't reach this one" failures are dropped silently (404 no drive, 403 access-denied / non-member private channel, 423 admin-locked site, 400 stale/unresolvable id); only actionable failures (auth, throttling, 5xx, network) appear in `partialErrors[]`, so it stays signal-only. Fans out one `/groups/{id}/drive` + one `/teams/{id}/channels` call per joined team + member group, a `filesFolder` call per private/shared channel, five fixed activity calls, and one `/sites/{id}/drives` call per discovered site (all capped by `--max-groups`, default 100; raise carefully — large memberships can hit 429 throttling). `/me/followedSites` is not used — it 403s on this token. | `--max-groups`, `--count-files` | `GET /me/drives + /me/joinedTeams + /me/memberOf + /me/drive/sharedWithMe + per-group /groups/<id>/drive + per-team /teams/<id>/channels/<ch>/filesFolder + /me/drive/recent + /me/drive/following + /me/insights/<trending|used|shared> + per-site /sites/<host>:/sites/<name>:/drives` |
|
|
@@ -128,9 +128,9 @@ For everything else:
|
|
|
128
128
|
| `convert-mail-attachment-to-pdf` | Convert an Outlook mail attachment to PDF on the fly. Polymorphic on the attachment’s `@odata.type`: fileAttachment uploads the bytes to a temp folder under /me/drive (large files use Graph’s chunked upload session — no 4 MB ceiling), runs ?format=pdf, then deletes the temp item; referenceAttachment resolves via /shares/{token}/driveItem and runs ?format=pdf in place; plain-text source extensions and `pdf` sources short-circuit to a raw-bytes envelope on either path (Graph’s `?format=pdf` does not accept `pdf` as an input format — pdf attachments are returned as-is). itemAttachment (embedded mail/event/contact) is unsupported here — Graph rejects those source types — use convert-mail-attachment-to-markdown instead. Worst-case wall-clock for huge attachments is ~22 minutes (1 metadata GET + up-to-20 chunk PUTs + 1 convert GET + 1 cleanup DELETE, each capped at 60s). | `--message-id`, `--attachment-id` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
|
|
129
129
|
| `convert-mail-attachment-zip-to-markdown` | Unzip a `.zip` Outlook mail attachment and convert every contained file in one call — the mail-side mirror of `convert-drive-item-zip-to-markdown`, so reading a zipped vendor deck doesn't need `get-mail-attachment` + manual `unzip` + per-file conversion. Pulls the fileAttachment bytes, unzips them (legacy GBK / CP437 entry names — Chinese vendor archives written by WinRAR / Windows Explorer — are decoded correctly, not mojibaked), and runs each file through the local pipelines: Office files (docx/xlsx/pptx/odt/ods/odp and macro-enabled / template variants) → markdown; plain-text entries decoded inline; legacy OLE .xls (sheetjs) and .doc (word-extractor, text only) extracted; an inner Outlook .msg rendered; PDFs have their text layer extracted; images, binaries, nested archives, legacy .ppt, and scanned/image-only PDFs are listed with a note (not unpacked) so one unsupported entry never fails the whole archive. Pass `--include-metadata true` to append each Office file's side-channel metadata block. Capped at 100 entries; beyond that the response is flagged `truncated`. itemAttachment / referenceAttachment are rejected (no inline zip payload). | `--message-id`, `--attachment-id`, `--include-metadata`, `--keep-quoted` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
|
|
130
130
|
| `convert-mail-to-markdown` | Render a single Outlook email as markdown — headers (`**Subject:**`, `**From:**`, `**To:**`, `**Cc:**` only when present, `**Date:**`), followed by the body run through turndown. By default NO image bytes are fetched: every inline `cid:` image renders as a readable `[inline image: <name>]` placeholder and the images surface in the file-attachments list, so the output stays close to the text size (an email whose 6 KB body carried 30 KB of signature-image base64 now ships at ~6 KB). Pass `--inline-images true` to embed inline images (`isInline:true` + `image/*` content-type, size ≤ 2 MB) as base64 `data:` URIs for self-contained output (non-image inline attachments are never embedded; oversize inline images keep a placeholder note; a cid whose per-image fetch fails degrades to the placeholder too). File attachments are always listed below the body by name + size + id; their bytes are NOT fetched here — call `convert-mail-attachment-to-pdf` or `get-mail-attachment` with the id when you actually need them. Staged-fetch design: one call for the body, one for the attachments-metadata list (only if `hasAttachments:true`), and with `--inline-images true` one per small inline image — replaces the old `?$expand=attachments` which timed out / truncated on messages with multi-MB attachments. | `--message-id`, `--inline-images`, `--keep-quoted` | `GET /me/messages/{message-id}` |
|
|
131
|
-
| `create-forward-draft` | Create an UNSENT forward draft of an existing message. POST /me/messages/{id}/createForward mints the draft (FW: subject, quoted original) with your comment placed above the quote and the recipients set, in one call. Redirects a thread to the right owner without leaving the CLI. The draft is saved in Drafts and can be reviewed, edited, and sent from any Outlook client; the CLI still cannot send. | `--forward-message-id`, `--to-recipients`, `--cc-recipients`, `--
|
|
131
|
+
| `create-forward-draft` | Create an UNSENT forward draft of an existing message. POST /me/messages/{id}/createForward mints the draft (FW: subject, quoted original) with your comment placed above the quote and the recipients set, in one call. Redirects a thread to the right owner without leaving the CLI. The draft is saved in Drafts and can be reviewed, edited, and sent from any Outlook client; the CLI still cannot send. | `--forward-message-id`, `--to-recipients`, `--cc-recipients`, `--comment`, `--subject`, `--body-content-type` | `POST /me/messages/{forward-message-id}/createForward (+ optional body-free PATCH for cc / subject)` |
|
|
132
132
|
| `create-mail-draft` | Create a new mail draft. POST /me/messages (or /me/mailFolders/{id}/messages when --mail-folder-id is set). The draft is saved in the Drafts folder (or the specified folder) and can be sent later via the Outlook client or Graph sendMail. Recipients are comma-separated email addresses. Returns a slim confirmation (id, subject, recipients, importance, bodyPreview, …) - NOT the body you just wrote; read the full draft back with get-mail-message if you need it. Use the returned id with update-mail-draft to modify before sending. | `--subject`, `--body-content`, `--body-content-type`, `--to-recipients`, `--cc-recipients`, `--bcc-recipients`, `--importance`, `--mail-folder-id` | `POST /me/messages (or /me/mailFolders/{mail-folder-id}/messages)` |
|
|
133
|
-
| `create-reply-draft` | Create an UNSENT reply draft threaded on an existing message. POST /me/messages/{id}/createReplyAll mints the draft (inherited recipients, RE: subject, quoted history) with your reply text placed above the quote, in one call. Reply-all by default - dropping recipients is a deliberate act, so pass --reply-all false to reply to the sender only, which switches the action to createReply. The draft is saved in Drafts and can be reviewed, edited, and sent from any Outlook client; the CLI still cannot send. | `--reply-to-message-id`, `--
|
|
133
|
+
| `create-reply-draft` | Create an UNSENT reply draft threaded on an existing message. POST /me/messages/{id}/createReplyAll mints the draft (inherited recipients, RE: subject, quoted history) with your reply text placed above the quote, in one call. Reply-all by default - dropping recipients is a deliberate act, so pass --reply-all false to reply to the sender only, which switches the action to createReply. The draft is saved in Drafts and can be reviewed, edited, and sent from any Outlook client; the CLI still cannot send. | `--reply-to-message-id`, `--comment`, `--body-content-type`, `--subject`, `--reply-all` | `POST /me/messages/{reply-to-message-id}/createReplyAll, or /createReply when {reply-all} is false (+ optional body-free PATCH for subject)` |
|
|
134
134
|
| `extract-mail-attachment-images` | Extract the embedded images from an Outlook mail attachment that is a pdf or a docx / xlsx / pptx (and their macro-enabled / template variants). OOXML reads the media parts directly (png/jpg/gif/bmp/tiff/webp/svg), including full-resolution / un-cropped originals and images on hidden slides; pdf walks every page via unpdf and re-encodes each painted image as PNG (page-oriented — not layer-hidden/unpainted/uncropped originals). fileAttachment decodes the inline bytes; referenceAttachment resolves via /shares/{token}/driveItem and fetches the content. Pair with the global output-dir flag to write every image to a folder; otherwise the bytes ride back base64-encoded. svg rides back as its XML source (which carries the diagram text labels); legacy vector (emf/wmf) and audio/video are skipped. itemAttachment and unsupported formats return a 415. | `--message-id`, `--attachment-id` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
|
|
135
135
|
| `extract-sharepoint-links-in-mail` | Find every `*.sharepoint.com` URL in the body of a single Outlook email and resolve each one to its driveItem (driveId, itemId, name, webUrl) so the agent can feed those into `download-drive-item-as-pdf` / `-as-markdown` etc. Read-only — no conversion happens here. Capped at 25 unique URLs per call to bound fan-out (returns `truncated: true` and `skippedCount` when the body has more); duplicate URLs are deduplicated. Per-link errors are captured inside each entry instead of failing the whole call. | `--message-id` | `GET /me/messages/{message-id}` |
|
|
136
136
|
| `find-mail-drafts` | Find existing drafts on a mail thread WITHOUT trusting a conversationId $filter. Reply and forward drafts do not always inherit the inbound message conversationId (a thread can split across several), and Graph $filter on the Drafts folder is not read-your-writes consistent, so filtering Drafts by conversationId misses drafts. This command instead scans the 50 most recently modified drafts and matches them client-side on a normalized subject (stripping RE:/FW: and localized reply/forward prefixes) plus, optionally, a shared recipient. Use it before create-reply-draft to avoid creating a duplicate: if a match comes back, revise it with update-mail-draft instead of making a new one. Read-only. | `--subject`, `--to-recipients` | `GET /me/mailFolders/drafts/messages?$top=50&$orderby=lastModifiedDateTime desc&$select=id,subject,toRecipients,ccRecipients,conversationId,lastModifiedDateTime,webLink (a read-only scan of the 50 most recently modified drafts; each is matched CLIENT-SIDE on a normalized {subject} and, when given, a shared {to-recipients} address, so neither value is sent to Graph)` |
|
|
@@ -148,7 +148,7 @@ For everything else:
|
|
|
148
148
|
| `list-mail-attachments` | List the attachments (file, item, reference) on a single Outlook message. The CLI ships an opinionated default `--select=id,name,contentType,size,isInline` so an LLM that doesn't slim the response itself doesn't accidentally pull multi-MB `contentBytes` for every attachment (a single 1.5 MB image attachment would otherwise blow the context window). The `@odata.type` discriminator is always returned by Graph regardless of `$select` (and Graph rejects asking for it explicitly). To fetch the actual bytes, call `get-mail-attachment` for the one you need (or override `--select` if you really want the raw inline payload). | `--message-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/messages/{message-id}/attachments` |
|
|
149
149
|
| `list-mail-child-folders` | List the subfolders of a single Outlook mail folder (e.g. subfolders of Inbox). | `--mail-folder-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders/{mail-folder-id}/childFolders` |
|
|
150
150
|
| `list-mail-folder-messages` | List the messages inside a specific Outlook mail folder (Inbox, custom folder, etc.). | `--mail-folder-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders/{mail-folder-id}/messages` |
|
|
151
|
-
| `list-mail-folder-messages-delta` | Track incremental changes (added / updated / deleted messages) within a single mail folder using Microsoft Graph delta tokens. The first call returns the current snapshot plus a `@odata.deltaLink`; subsequent calls with that link return only what has changed since. | `--mail-folder-id`, `--top`, `--
|
|
151
|
+
| `list-mail-folder-messages-delta` | Track incremental changes (added / updated / deleted messages) within a single mail folder using Microsoft Graph delta tokens. The first call returns the current snapshot plus a `@odata.deltaLink`; subsequent calls with that link return only what has changed since. `--top` is translated into the `Prefer: odata.maxpagesize=N` header: as a `$top` query parameter Graph reads a satisfied count as "sync complete" and hands back a deltaLink after N items, silently abandoning the rest of the folder. `$skip` and `$orderby` are NOT exposed — Graph ignores the former on this endpoint and rejects the latter unless it merely restates the default `receivedDateTime desc`. | `--mail-folder-id`, `--top`, `--select`, `--filter`, `--expand` | `GET /me/mailFolders/{mail-folder-id}/messages/delta()` |
|
|
152
152
|
| `list-mail-folders` | List the top-level mail folders in the signed-in user’s Outlook mailbox (Inbox, Sent Items, etc.). | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders` |
|
|
153
153
|
| `list-mail-folders-delta` | Track incremental changes to the mail-folder tree itself (folders added / renamed / deleted). The first call returns the current snapshot plus a `@odata.deltaLink`; subsequent calls with that link return only what has changed. Companion to `list-mail-folder-messages-delta` which tracks message changes inside one folder. Note: Graph explicitly rejects `$top`, `$filter`, `$orderby`, and `$search` on this delta endpoint (`ErrorInvalidUrlQuery: not supported with change tracking over the 'Folders' resource`), so the OData passthrough is intentionally NOT exposed here. | _(none)_ | `GET /me/mailFolders/delta()` |
|
|
154
154
|
| `list-mail-messages` | List the most recent messages from across the signed-in user's entire Outlook mailbox (every folder including Sent, Archive, Junk; default sort `receivedDateTime` desc). The CLI ships a slim default `--select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,hasAttachments,isRead,importance,bodyPreview,conversationId` (`conversationId` groups messages into a thread and can be handed to `list-conversation-messages`) so a page of 25 messages stays ~30-60 KB instead of ~1 MB. Pass `--select id,subject,body` (or any other comma-separated field list) to override. Use `list-mail-folder-messages` to scope to a single folder such as Inbox. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/messages` |
|
|
@@ -159,7 +159,7 @@ For everything else:
|
|
|
159
159
|
| `read-mail-attachment` | Read an Outlook mail attachment whatever it is — one command that auto-routes by file type, preferring the content-type when the filename extension is misleading (a real `.jpg` that is actually a spreadsheet still converts), so a caller never has to choose between the convert-mail-attachment-* siblings. A `.zip` fileAttachment is unpacked and every entry converted (mirrors `convert-mail-attachment-zip-to-markdown`, returning the `{ count, files }` envelope; legacy GBK/CP437 names decoded). Any other attachment — docx/xlsx/pptx/odt/ods/odp + macro/template variants → markdown, csv → table, pdf → text layer (with `pageCount`), legacy .xls/.doc extracted, an inner Outlook .msg rendered recursively (quoted chain stripped unless `--keep-quoted true`), plain text passed through, referenceAttachment resolved via `/shares`, and itemAttachment (embedded mail/event/contact) rendered — goes through the same dispatch as `convert-mail-attachment-to-markdown` (returning its `{ contentType, size, text }` envelope). Images, scanned/image-only PDFs, and legacy .ppt return an actionable 415 pointing at `convert-mail-attachment-to-pdf` + a vision model or `get-mail-attachment` for the raw bytes. Pass `--include-metadata true` to append Office side-channel metadata. Use the explicit `convert-mail-attachment-to-markdown` / `-to-pdf` / `-zip` siblings only when you need to force a specific output format. | `--message-id`, `--attachment-id`, `--include-metadata`, `--keep-quoted` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
|
|
160
160
|
| `resolve-mail-link` | Parse a Microsoft Outlook web mail link (the URL emitted by the "Copy link" / address-bar share of an email) into its `messageId`. Pure transformation — no Graph call. Pipe the result into `get-mail-message` to fetch the body, or `convert-mail-to-markdown` to render it. For Outlook calendar links use `resolve-calendar-link` instead — this command rejects them with a pointer. | `--url` | `GET {url}` |
|
|
161
161
|
| `search-mail-messages` | Search the signed-in user's entire Outlook mailbox using KQL or free text. Results are ranked by Graph relevance. The CLI ships a slim default `--select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,hasAttachments,isRead,importance,bodyPreview,conversationId` (same as `list-mail-messages`; `conversationId` is included so you can group hits into a thread or feed one to `list-conversation-messages`) so a 3-result page stays ~3 KB instead of ~30 KB. Pass `--select id,subject,body` to widen, or override entirely. Note: Graph does not allow `$search` and `$filter` together — the CLI rejects `--filter` client-side with a pointer to `list-mail-messages` (which supports OData filtering). For sorting, server-side `$orderby` is also not allowed with `$search`; use the relevance ranking Graph returns. **Exact-phrase search works**: `--query '"budget allocation"'` and embedded field phrases like `--query 'subject:"Contoso A2 & B7 timeline"'` are supported — the CLI escapes your double quotes into KQL phrase quotes, wraps the whole expression in the `"…"` Graph requires, and percent-encodes the value so `&`, `#`, and `+` are wire-safe. Pass raw KQL otherwise, e.g. `--query 'subject:invoice from:alice'`. | `--query`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/messages?$search="{query}"` |
|
|
162
|
-
| `update-mail-draft` | Update an existing mail draft. PATCH /me/messages/{id} — modifies a draft created by create-mail-draft (or any existing draft in the Drafts folder). Only the fields you pass are updated; omitted fields are left unchanged. At least one field must be provided. On a THREADED draft (one made by create-reply-draft / create-forward-draft), revise your text with --comment, which rewrites only what sits above the quoted history and leaves the quote byte-identical; --body-content
|
|
162
|
+
| `update-mail-draft` | Update an existing mail draft. PATCH /me/messages/{id} — modifies a draft created by create-mail-draft (or any existing draft in the Drafts folder). Only the fields you pass are updated; omitted fields are left unchanged. At least one field must be provided. On a THREADED draft (one made by create-reply-draft / create-forward-draft), revise your text with --comment, which rewrites only what sits above the quoted history and leaves the quote byte-identical; --body-content replaces the whole body and would drop the thread, so it is REFUSED on a draft that still carries a quote unless you pass --replace-quoted-history true. Passing an EMPTY string to a recipient flag clears that list, which is how you drop recipients a reply-all or forward inherited; omitting the flag leaves the list alone. Returns a slim confirmation (id, subject, recipients, importance, bodyPreview, …) - NOT the full body, which you just wrote; read it back with get-mail-message if you need the whole draft before sending. | `--message-id`, `--subject`, `--body-content`, `--comment`, `--replace-quoted-history`, `--body-content-type`, `--to-recipients`, `--cc-recipients`, `--bcc-recipients`, `--importance` | `PATCH /me/messages/{message-id} (+ a GET of body,isDraft first when {comment} is used, or when {body-content} is used without {replace-quoted-history})` |
|
|
163
163
|
|
|
164
164
|
### Notes (OneNote)
|
|
165
165
|
|
|
@@ -175,7 +175,7 @@ For everything else:
|
|
|
175
175
|
| `list-sharepoint-site-onenote-notebook-sections` | List sections inside one OneNote notebook attached to a SharePoint site. | `--site-id`, `--notebook-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/onenote/notebooks/{notebook-id}/sections` |
|
|
176
176
|
| `list-sharepoint-site-onenote-notebooks` | List OneNote notebooks attached to a SharePoint site (separate from the personal `list-onenote-notebooks` which targets `/me`). | `--site-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/onenote/notebooks` |
|
|
177
177
|
| `list-sharepoint-site-onenote-section-pages` | List pages inside one section of a SharePoint-site OneNote notebook. | `--site-id`, `--onenote-section-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/onenote/sections/{onenote-section-id}/pages` |
|
|
178
|
-
| `search-onenote-pages` | Find OneNote pages whose title contains a substring (case-sensitive — page content is NOT searched). Microsoft removed full-text OneNote `?search=` from v1.0 Graph; only $filter against `title` remains, which is what this command runs. Accepts the OData passthrough flags top/skip/select/orderby/expand. The filter passthrough is intentionally omitted — the path already pins a `$filter` for the title-contains predicate, and Graph rejects two `$filter` query params. | `--
|
|
178
|
+
| `search-onenote-pages` | Find OneNote pages whose title contains a substring (case-sensitive — page content is NOT searched). Microsoft removed full-text OneNote `?search=` from v1.0 Graph; only $filter against `title` remains, which is what this command runs. Accepts the OData passthrough flags top/skip/select/orderby/expand. The filter passthrough is intentionally omitted — the path already pins a `$filter` for the title-contains predicate, and Graph rejects two `$filter` query params. | `--query`, `--top`, `--skip`, `--select`, `--orderby`, `--expand` | `GET /me/onenote/pages?$filter=contains(title,'{query}')` |
|
|
179
179
|
|
|
180
180
|
### User
|
|
181
181
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ask-marcel-office-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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>",
|