npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ask-marcel-office-cli

v1.5.0

Published

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.

Readme

ask-marcel-office-cli

A Microsoft Graph CLI built for LLMs. 177 commands across Mail, Calendar, OneDrive, SharePoint, Excel, Teams chats, Planner / To-Do, OneNote, and directory — plus local-file tools (markdown conversion, image extraction) that need no sign-in at all. Sign in once with your Microsoft 365 account — no Azure app registration, no admin consent, no client secrets.

npm i -g ask-marcel-office-cli
ask-marcel login                 # browser opens once, token cached
ask-marcel my-quick-context      # who am I + my IDs, in one round trip
ask-marcel list-mail-messages --top 5

Why it exists

LLM tool-loops keep hitting the same three walls with Microsoft Graph:

  1. Auth is a project. Register an app, get tenant-admin consent, manage secrets, refresh tokens — before the first API call.
  2. Default payloads are tuned for backend services, not context windows. Listing endpoints return every field on every item, used-range Excel calls return four redundant 2D arrays, attachment endpoints inline base64 by default. An agent that reads "what's in my inbox" without trimming burns its budget on metadata it never needed.
  3. Errors are opaque. BadRequest: Invalid filter clause doesn't tell a model what to fix.

ask-marcel fixes all three at the CLI layer, so the model just calls commands and reads back-pressure-friendly responses.

What you get

Read-only by design

This is the most important property. 173 GET endpoints + 2 POST (searches) + 1 POST (create draft) + 1 PATCH (update draft) = 177 commands. No send-mail, no create-event, no upload-file, no delete-anything. The only write operations are draft creation and update — a hallucinated command can at most create an unsent draft in your Drafts folder. Safe default for autonomous agents, MCP servers, and "let Claude poke around my mailbox" sessions where you can't fully review every tool call.

One call gets the full email context

A typical "read this email" loop in raw Graph: GET the message → GET the attachments list → GET each attachment's bytes → scan the body HTML for sharepoint.com URLs → resolve each URL to a driveItem → GET each driveItem. Six round-trips minimum, plus HTML-to-text conversion the LLM has to do itself.

convert-mail-to-markdown collapses that into one call:

  • Body rendered as markdown (turndown pipeline)
  • Quoted reply chains / forwarded-message blocks stripped by default so a long thread doesn't duplicate earlier messages into the model's context (the cut is replaced with a visible marker; opt out with --keep-quoted true to keep the full body)
  • Inline images embedded as base64 data: URIs (size-capped per image — opt out with --inline-images false to keep raw cid: refs)
  • File attachments listed below the body with id + name + size, ready for follow-up calls
  • Pair with extract-sharepoint-links-in-mail to resolve every SharePoint URL in the body to its driveItem in parallel (capped at 25 unique URLs per call)

Office docs → markdown or PDF on the fly

Feed any Office-shaped file (docx, xlsx, pptx, csv, rtf, odt, …) into the local conversion pipeline OR through Graph's ?format=pdf when slide layout and images matter:

  • download-drive-item-as-markdown — docx via mammoth (embedded images become [image] placeholders by default — --inline-images true to embed them as base64, or pull the full-resolution originals with extract-drive-item-images), xlsx as one markdown table per sheet (a sheet whose used range exceeds the --max-cells cap, default 50 000, becomes a band-by-band read hint instead of a multi-hundred-MB table that would OOM), csv as a table, odt/ods/odp via content.xml (headings, lists, tables, named sheets, per-slide text, with office:annotation comments folded inline), pptx flattened to per-slide text (titles + bullets + text boxes + table cells, speaker notes inline, as ## Slide N sections — download-drive-item-as-pdf + a vision model when layout / images matter), pdf via text-layer extraction (unpdftext/plain; a scanned / image-only PDF with no text layer points you at download-drive-item-as-pdf + a vision model), legacy OLE Office (.xls read by sheetjs like .xlsx; .doc extracted by word-extractor as plain text; .ppt has no pure-JS path → convert to PDF first), plain-text passthrough
  • download-drive-item-as-pdf — Graph PDF conversion for anything it supports (preserves slide layout, images, charts — the right call for pptx and image-heavy docs)
  • convert-mail-attachment-to-markdown / convert-mail-attachment-to-pdf — same pipelines but starting from an email attachment
  • read-mail-attachment — one call that reads any mail attachment, auto-routing by file type (a .zip is unpacked and every entry converted; docx/xlsx/pptx/odf/csv/PDF/.msg/legacy/text → markdown; images, scanned PDFs, and legacy .ppt return an actionable 415 pointing at the raw-bytes / vision route) so an agent never has to choose between the convert-mail-attachment-* siblings
  • convert-local-file — same pipelines but starting from a file on disk (--path ./report.docx); never calls Graph (works offline, no login). A .zip is unpacked with every contained file converted in one call. The two things it can't do locally — convert to PDF and Loop/Fluid/Whiteboard sources — need Graph's server-side renderer (upload to OneDrive and use the drive-item siblings)
  • convert-drive-item-zip / convert-mail-attachment-zip — unzip an archive (OneDrive/SharePoint item, or an Outlook attachment) and convert every contained file in one call; legacy GBK / CP437 entry names (Chinese vendor archives from WinRAR / Windows Explorer) are decoded correctly, never mojibaked; unsupported entries are listed with a note instead of failing the archive
  • Outlook .msg files (saved/forwarded emails) convert to markdown through every entry point above — H1 subject, From/To/Cc/Date header block, the body, and an ## Attachments section where each attachment is itself converted recursively (depth-capped)
  • extract-sharepoint-links-in-documents — the doc-side sibling of extract-sharepoint-links-in-mail: resolve every *.sharepoint.com URL embedded in a docx/xlsx/pptx (read from the package's relationship parts) or an odt/ods/odp (read from the inline xlink:href links in content.xml) to its driveItem, so an agent can follow references out of a document the same way it follows them out of an email

Pass --include-metadata true on any *-as-markdown (or convert-mail-attachment-to-markdown) command to surface the side-channel content the rendered body hides. For docx (## DOCX metadata): core/app/custom doc properties, people registry, external hyperlinks, comments (each quoting the document text span it annotates), tracked changes, hidden text (w:vanish), MERGEFIELD / HYPERLINK / DOCVARIABLE instructions, bookmarks. For xlsx (## Workbook metadata): properties, external relationships, defined names, hidden / very-hidden sheets, legacy + threaded cell comments (each tagged with its cell), the persons registry. For pptx (## PPTX metadata): properties, external relationships, slide tags, comment authors + comments (legacy + modern, each anchored to its slide), and per-slide title / speaker notes / hidden flag — appended after the per-slide text body (use download-drive-item-as-pdf + a vision model for slide visuals / layout). Each family also covers its macro-enabled (.docm / .xlsm / .pptm) and template (.dotx / .xltx / .potx, etc.) variants, and surfaces a ### Macros (VBA) section flagging an embedded vbaProject.bin (the file can execute code on open). For OpenDocument (.odt / .ods / .odp) the flag appends a ## OpenDocument metadata block (Dublin Core + ODF properties, keywords, user-defined custom fields) after the converted body. No-op on other sources.

Extract embedded images from documents

extract-drive-item-images (OneDrive / SharePoint), extract-mail-attachment-images (Outlook attachments), and extract-local-file-images (a file on disk — no Graph, no login) pull the embedded images out of a docx, xlsx, pptx, or pdf. For Office files it reads the OOXML media parts (png/jpg/gif/bmp/tiff/webp/svg) — including original full-resolution / un-cropped originals and images on hidden slides that the rendered view never shows. SVG rides back as its XML source (which carries the diagram's own text labels); legacy vector (emf/wmf) and audio/video are skipped. For a PDF it walks every page via unpdf (a pure-JS, no-native-deps pdf.js build) and re-encodes each painted image as PNG — page-oriented, so it captures images as drawn on each page (it does not reach layer-hidden/unpainted XObjects or the full uncropped original behind a clipped image). Pair with the global --output-dir <dir> to write every image to a folder (the directory is auto-created and each base64 becomes a savedTo path); without it the bytes ride back base64-encoded so a vision model can read them directly.

The CLI follows any SharePoint media-transform redirect internally, so the LLM never has to fetch an external URL.

Find every drive you can reach

list-drives only returns your personal OneDrive(s). list-accessible-drives unions every discovery vector the delegated token can hit — /me/drives (personal), /me/joinedTeams (Teams libraries), /me/memberOf Unified groups → each group's drive (SharePoint M365-group sites), /me/drive/sharedWithMe (drives behind files shared with you), per-team /teams/{id}/channelsfilesFolder for private/shared channels (which live in their own sites, not the team default drive), activity signals (/me/drive/recent, /me/drive/following, /me/insights/{trending,used,shared}), and every non-default document library of each discovered site via a path-addressed /sites/{host}:/sites/{name}:/drives (catches secondary libraries the default-drive vectors skip) — deduped by drive id and tagged with the sources[] that surfaced each one (channel = private/shared channel drive, activity = a recently-used/followed/trending item drive, siteLibrary = a non-default site library). These vectors catch OneDrives, channel sites, and direct-link sites the tenant search index (search-sharepoint-sites-by-name) never returns; the index in turn returns sites you can open but aren't a member of. For that index half, search-all-accessible-sites deep-pages the Microsoft Search API (POST /search/query, entityTypes: ['site']) past the single-page cap of search-sharepoint-sites-by-name, returning the full security-trimmed site index (on one tenant: ~154 sites vs 80). So the union of search-all-accessible-sites + list-accessible-drives is the practical maximum on a delegated token (truly enumerating every site in the tenant needs tenant-admin app-only /sites/getAllSites). Both site-search commands exclude archived sites: each result is probed (GET /sites/{id}?$select=…,siteCollection) and dropped when Graph reports it archived or fails with 423 resourceLocked — the signal a departed/unlicensed user's auto-archived OneDrive returns (no more sharepointerror.aspx?scenario=SiteArchived dead links in the output); the count surfaces as archivedExcluded. --max-groups caps every fan-out, and partialErrors[] stays signal-only: benign "can't reach this one" results (404 no-drive, 403 access-denied / non-member channel, 423 admin-locked site, 400 stale id) are dropped silently — only actionable failures (auth, throttling, 5xx, network) are listed. Both commands also surface a best-effort fileEstimate — the Microsoft Search index's security-trimmed driveItem count, i.e. roughly how many files you can access across all of SharePoint/OneDrive (index-wide, not limited to the listed drives).

Browser-OAuth at first launch

No Azure app, no tenant admin. The CLI captures the same token the Teams web client uses — works for any Microsoft 365 account, personal or enterprise.

Login flow: By default, the CLI uses Playwright to open a browser for authentication. Alternatively, you can use the Ask Marcel Companion browser extension for faster authentication.

ask-marcel login              # default: Playwright browser
ask-marcel login --use-extension  # use browser extension (requires setup)

Browser extension setup (optional, one-time):

  1. Open chrome://extensions/ (Chrome) or edge://extensions/ (Edge)
  2. Enable "Developer mode" (top-right toggle)
  3. Click "Load unpacked" → select the browser-extension/ folder from this repo
  4. Enable in incognito/inprivate mode: Click the extension's "Details" button, then enable "Allow in incognito" (Chrome) or "Allow in InPrivate" (Edge). This is required — the CLI opens an incognito/inprivate window, and extensions are disabled there by default.
  5. Done — use ask-marcel login --use-extension to authenticate via the extension

Stable error envelope with actionable hints

Every failure — Graph, CLI parser, Zod validation, substrate — comes back as {ok: false, error, errorCode?, hint?, source, retryAfterSeconds?}. The hint field tells the model what to do next (e.g. "string literals MUST use single quotes; embed one by doubling it") and source tells it where the failure came from. Curated rules for 20+ recurring Graph errors plus cross-resolver pointers (passed a Teams URL to resolve-mail-link? Hint says "re-run with resolve-teams-link"). When Graph throttles (HTTP 429, sometimes 503) the response's Retry-After is surfaced as retryAfterSeconds — the integer seconds to wait — so a caller running tenant-scale crawls can honor the server's backoff instead of guessing.

Lean responses

Listings ship with hand-tuned --select defaults — a mail listing returns id, subject, from, to, cc, dates, read-state, importance, bodyPreview rather than every field on every message. get-excel-used-range returns the values array instead of the four 2D arrays Graph emits. Opt out per call with --full true, or override with your own --select id,subject,body.

Saves big binaries to disk so they never hit the model's context

A typical conversion command returns multi-MB PDF bytes. Sending 5 MB of base64 through stdout would blow most context windows AND quadruple the token bill.

Two flag patterns avoid the round-trip:

  • --output-path /path/to/file.pdf — the CLI decodes the bytes, writes them to disk, and replaces base64: "..." in the envelope with savedTo: "/path/to/file.pdf". The LLM sees a 3-line confirmation instead of a 7-million-character payload. Works on every command that returns binary or text content; rejected with a clear error on plain-JSON commands so a misapplied flag is never silent. A binary payload over ~1 MB is refused without this flag (an inline_too_large error pointing you here), so a multi-MB base64 string can never flood the context by accident.
  • No flag, text mode — binary commands print a one-line summary (binary: application/pdf, 4837291 bytes — use --output-path to save) instead of spilling base64 to stdout. The LLM sees a hint without ever pulling the bytes.

Relative dates on calendar windows

--start-date-time "start-of-week" --end-date-time "+7d". No timestamp math before answering "what's on my calendar this week".

30-second quickstart

# install (Bun ≥1.0 or Node ≥20)
npm i -g ask-marcel-office-cli

# authenticate (cached → refresh → browser fallback)
ask-marcel login

# the rest is read-only (the only writes are mail drafts) and discoverable from --help
ask-marcel list-drives
ask-marcel search-onedrive-files --drive-id "b!abc..." --query "Q3 budget"
ask-marcel convert-mail-to-markdown --message-id "AAMkAD..."
ask-marcel list-calendar-view --start-date-time today --end-date-time +7d
ask-marcel convert-mail-attachment-to-pdf \
  --message-id "AAMkAD..." --attachment-id "AAMkAD...attach1" \
  --output-path /tmp/deck.pdf

Asking the CLI what it can do

Five discovery surfaces, each tuned for a different audience and token budget:

| When you want | Run | Returns | |---|---|---| | Help with a single command | ask-marcel <command> --help | Required flags, optional flags, an example, pagination notes | | A scan of every command | ask-marcel --help | One-sentence summary per command, grouped by category | | The slim LLM-friendly index | ask-marcel help-json --terse | JSON manifest with heavy fields (options, response shape) stripped — best first-call for an agent meeting the CLI for the first time | | The slim index for one domain | ask-marcel help-json --terse --category mail | Same as above, filtered to one of 12 categories — keeps the response tiny when the agent already knows the domain | | Rich docs for one command | ask-marcel docs <command> | Full Markdown to stdout (response shape, examples, the underlying Graph endpoint, Microsoft Learn link) |

Pair help-json --terse --category <name> with docs <command> for the canonical agent loop: scan the category, pick a command, fetch its full docs, then call it.

Use it from Claude Code, Cursor, Cline, or any tool-calling LLM

Most agents already know how to read JSON from stdout. Two patterns work:

1. Drop in as a shell tool — the agent learns the manifest, then runs ask-marcel <command> --output json. The slim defaults + structured error hints mean it can self-recover from typos.

2. Embed as a library — every command is exported. Compose it inside your own MCP server, Claude Agent, or LangChain tool:

import { commands, buildDeps } from 'ask-marcel-office-cli';

const { graph } = buildDeps();
const result = await commands['list-mail-messages'].execute(graph, { top: '10' });
if (result.ok) {
  // result.value is the Graph payload — typed Result<unknown, GraphError>
}

Auth — two paths

Most users — use the built-in browser-OAuth ladder:

import { buildDeps } from 'ask-marcel-office-cli';

const { graph } = buildDeps();
// First call triggers cache → refresh → headed-Chromium fallback automatically.
// Tokens cached at ~/.ask-marcel/token-cache.json for subsequent calls.

Agents / CI / MCP servers — bring your own token:

import { createGraphClient } from 'ask-marcel-office-cli';

const graph = createGraphClient({
  getAccessToken: async () => ({
    ok: true,
    value: await fetchTokenFromYourVault(),
  }),
  logout: async () => ({ ok: true, value: undefined }),
});

The AuthManager interface is two async methods that return Result<T, AuthError>. Plug in any token source — Azure Managed Identity, a secrets vault, an on-behalf-of flow, hand-pasted JWTs in tests. The Graph client doesn't care where the token came from.

Deep docs

  • All 177 commands — per-category tables with required params + Graph endpoint
  • Usage guide — output formats, OData passthrough, --output-path, pagination, library API, architecture, configuration, quality gates
  • Machine-readable manifest — JSON for programmatic discovery (LLM tool-loops, IDE plugins, MCP servers); also importable via import manifest from 'ask-marcel-office-cli/commands.json'
  • QA playbook — the repeatable full-surface health-check procedure (offline gates, parameter matrix, conversion contracts, live Graph drift probes) used to audit each release

Agent skill (progressive disclosure)

A Codex skill lives at .agents/skills/ask-marcel-office/ and teaches agents how to use the CLI without loading all 177 commands into context at once.

Structure

.agents/skills/ask-marcel-office/
├── SKILL.md                          # core workflow + category index
└── references/                       # per-domain command details, loaded on demand
    ├── marcel-mail.md        (32 commands)
    ├── marcel-drive.md       (30 commands)
    ├── marcel-calendar.md    (23 commands)
    ├── marcel-sharepoint.md  (18 commands)
    ├── marcel-user.md        (15 commands)
    ├── marcel-tasks.md       (15 commands)
    ├── marcel-excel.md       (11 commands)
    ├── marcel-notes.md       (11 commands)
    ├── marcel-chats.md        (9 commands)
    ├── marcel-teams.md        (7 commands)
    └── marcel-meta.md         (6 commands)

How it works

SKILL.md contains the authentication flow, the discovery loop (help-json --terse --categorydocs <cmd> → execute), key patterns (OData passthrough, relative dates, document conversion, pagination), and a category index. The full command reference for each domain lives in references/marcel-<category>.md and is loaded only when the agent needs that domain — keeping the context window lean.

Roadmap

Read-only stays the default forever. There's no fixed feature backlog — coverage grows out of real LLM workflows as they come up.

Suggestions, requests, and pull requests welcome — see the issues page.

Built with

  • Bun + TypeScript — single binary install, Node ≥20 fallback. Result<T, E> at every IO boundary, branded value-object types at trust boundaries, classicist outside-in TDD, zero lint warnings, 100% coverage on every tier.
  • Microsoft Graph v1.0 — the public API surface, no beta endpoints in production code.
  • Playwright — headed Chromium for the first-launch browser-OAuth dance.

License

MIT © Vincent Delacourt