Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ jobs:
- name: Smoke test CLI commands with cached fixture
run: npm run smoke:fixture

# Keep the version in sync with ATK_VERSION in m365-plugin.yml.
- name: Install Microsoft 365 Agents Toolkit CLI
env:
ATK_VERSION: 1.1.18
run: npm install -g --no-audit --no-fund "@microsoft/m365agentstoolkit-cli@$ATK_VERSION"

- name: Smoke test M365 plugin bundle and app package
run: npm run smoke:m365 -- --package

- name: Smoke test live catalog
if: github.event_name != 'pull_request'
run: npm run smoke:live
130 changes: 130 additions & 0 deletions .github/workflows/m365-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: M365 plugin

# Builds the offline Microsoft 365 Copilot plugin app package (for Cowork) with live catalog data
# and publishes it as release m365-v<version>, marked Latest, so
# releases/latest/download/microsoft-build-m365.zip serves it. The M365 plugin has its own
# version, separate from the plugin manifests, because catalog refreshes need new releases too.
on:
workflow_dispatch:
inputs:
version:
description: M365 plugin version (x.y.z), higher than the last m365-v* release. Cowork only updates to a higher version.
required: true
type: string
events:
description: Events to bundle, separated by spaces (for example "build-2026 ignite-2026"). Leave empty for all events.
required: false
type: string
default: ''

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

env:
# Keep in sync with the atk install step in ci.yml.
ATK_VERSION: 1.1.18
ZIP_NAME: microsoft-build-m365.zip

jobs:
build:
name: Build M365 plugin app package
runs-on: ubuntu-latest
timeout-minutes: 20

defaults:
run:
working-directory: cli

steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
cache: npm
cache-dependency-path: cli/package-lock.json

- name: Check branch and version
env:
GH_TOKEN: ${{ github.token }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
VERSION: ${{ inputs.version }}
run: |
if [ "$GITHUB_REF" != "refs/heads/$DEFAULT_BRANCH" ]; then
echo "::error::Run this workflow from $DEFAULT_BRANCH; it publishes a public release."
exit 1
fi
if ! printf '%s' "$VERSION" | grep -Eq '^[1-9][0-9]*\.[0-9]+\.[0-9]+$'; then
echo "::error::Version \"$VERSION\" must look like 1.2.3 and can't start with 0."
exit 1
fi
tags="$(gh api --paginate "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/m365-v" --jq '.[].ref | ltrimstr("refs/tags/m365-v")')"
last="$(printf '%s\n' "$tags" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1 || true)"
if [ -n "$last" ] && { [ "$VERSION" = "$last" ] || [ "$(printf '%s\n' "$last" "$VERSION" | sort -V | tail -n 1)" != "$VERSION" ]; }; then
echo "::error::Version $VERSION must be higher than the last M365 release, $last."
exit 1
fi

- name: Install dependencies
run: npm ci

- name: Install Microsoft 365 Agents Toolkit CLI
run: npm install -g --no-audit --no-fund "@microsoft/m365agentstoolkit-cli@$ATK_VERSION"

- name: Build app package
env:
VERSION: ${{ inputs.version }}
EVENTS: ${{ inputs.events }}
run: |
args=(--plugin-version "$VERSION" --package)
IFS=', ' read -r -a events <<< "$EVENTS"
for event in "${events[@]}"; do
if [ -n "$event" ]; then args+=(--event "$event"); fi
done
npm run build:m365 -- "${args[@]}"
cp ../out/m365-plugin-app/appPackage/build/appPackage.zip "../out/$ZIP_NAME"

- name: Upload app package artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
path: out/${{ env.ZIP_NAME }}
archive: false
if-no-files-found: error

publish:
name: Publish release
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10

permissions:
contents: write

steps:
- name: Download app package artifact
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ env.ZIP_NAME }}
path: release
skip-decompress: true

- name: Create release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: |
unzip -l "release/$ZIP_NAME" manifest.json
gh release create "m365-v$VERSION" "release/$ZIP_NAME" \
--repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" \
--title "M365 plugin $VERSION" \
--generate-notes \
--latest
12 changes: 10 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The repo publishes plugin metadata for GitHub Copilot and Claude Code. Keep shar
## Sync rules

- Event support is duplicated by design between the CLI and skill docs. When adding, removing, or renaming events, update `cli\src\config.ts`, the supported/default event guidance in `skills\microsoft-build\SKILL.md`, and affected tests/docs together.
- The offline Microsoft 365 Copilot plugin (Cowork) builds its SKILL.md from `skills\microsoft-build\SKILL.md`; there's no second copy to keep aligned. `cli\m365-skill\offline-overlay.md` holds the only Cowork-specific text: frontmatter keys to replace and a `## Session catalog access` section that uses only `node scripts/msevents.mjs` (no `npx`, network, or `refresh`). `SKILL_REWRITES` in `cli\scripts\build-m365-plugin.mjs` rewrites the remaining online steps, and the build fails when a rewrite stops matching or online wording (`npx`, catalog endpoints, `refresh`, "live catalog") is left, so `npm run smoke:m365` catches main-skill edits that need a matching update there. Changing the overlay or rewrites doesn't need a plugin version bump; the M365 plugin has its own version, entered when the M365 plugin workflow publishes a release.
- Treat `skills\microsoft-build\SKILL.md` as the product contract for event-session behavior. For Build, Ignite, or event-session work: default "Build" to `build-2026`, get session metadata from the live catalog through `msevents` or the endpoint, get SDK/API facts from Microsoft Learn MCP, and never invent session IDs, speakers, schedules, or links.
- Versioning gate: any meaningful `skills\microsoft-build\SKILL.md` change (agent behavior, commands/examples agents may follow, supported/default events, CLI/MCP/catalog usage, or user-facing workflow guidance) must bump both versioned plugin manifests to the same value; patch bumps are fine for guidance-only changes.
- PR review gate: GitHub Copilot/code reviewers should flag meaningful `SKILL.md` changes without synced plugin version bumps, and any plugin version drift. Typo/format-only skill edits may skip the bump only when the PR explicitly says so.
Expand All @@ -36,16 +37,23 @@ npm test
npx vitest run test\search.test.ts
npx vitest run test\search.test.ts -t "finds sessions by title keyword"
npm run smoke:fixture
npm run smoke:m365
npm run smoke:m365 -- --package
npm run build:m365 -- --plugin-version 1.0.7 --package
```

`npm run smoke:live` hits the live catalog; CI only runs it outside pull requests.
`npm run smoke:live` hits the live catalog; CI only runs it outside pull requests. `npm run build:m365` also downloads live catalogs and writes the M365 plugin to `out\m365-plugin` (git-ignored); `--package` uses the `atk` CLI to build `out\m365-plugin-app\appPackage\build\appPackage.zip`. `smoke:m365 -- --package` also needs `atk`: it builds the zip from the fixture, unzips it and runs the offline checks on the unzipped skill, as CI does. Never commit generated snapshot data.

`.github\workflows\m365-plugin.yml` publishes the M365 plugin. It runs manually on `main` with a version input, which must be `x.y.z` and higher than every `m365-v*` tag; the M365 plugin is versioned separately from the plugin manifests. An optional events input (space-separated event IDs, empty for all) is passed to the build as `--event` flags. It creates release `m365-v<version>` with `microsoft-build-m365.zip` attached and marks it Latest, which the README links through `releases/latest/download/`, so don't mark other releases as Latest. Keep the build job read-only; only the job that creates the release gets `contents: write`.

The M365 plugin bundles `cli\src\index.ts` (the whole CLI) with esbuild. Its banner sets `MSEVENTS_OFFLINE=1` and points `MSEVENTS_CACHE_DIR` at the bundled `data\` folder, and the version comes from a build-time `__MSEVENTS_VERSION__` constant because the bundle has no `package.json`. Keep offline mode free of network calls and cache writes; `npm run smoke:m365` checks this with read-only data.

Note: The first time `npx @microsoft/events-cli` is run, it prompts for permission and can cause CLI tool calls to hang in AI agents. In all instructions or `SKILL.md`, prefer `npx -y @microsoft/events-cli` to include the `-y` flag.

## CLI behavior contracts

- Catalog data has inconsistent shapes: fields may be strings, `{ displayValue }` objects, arrays, or empty values. Normalize through the helpers in `cli\src\data\normalize.ts`; records without `sessionCode` are intentionally skipped.
- Cache behavior is part of the CLI contract. `ensureCache` loads cached sessions, revalidates when due, fetches missing event caches, and only falls back to stale cache when the stale data is scoped to the request. Tests use `MSEVENTS_CACHE_DIR` to isolate cache state.
- Cache behavior is part of the CLI contract. `ensureCache` loads cached sessions, revalidates when due, fetches missing event caches, and only falls back to stale cache when the stale data is scoped to the request. With `MSEVENTS_OFFLINE=1` it only reads the cache and skips uncached events; `refresh` errors and `status` reports offline. Tests use `MSEVENTS_CACHE_DIR` to isolate cache state.
- Search behavior is intentionally specialized: the `msevents sessions` command requires `--query`, `--tech`, or `--speaker`; code-like queries exact-match session codes and variants; technology search spans product/tags/topic/solution area/languages/title/description; speaker search post-filters on the queried last name.

## General principles
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,13 @@ Session results are a starting point. For broad topics, ask the agent to refine
| VS Code | Open Extensions (Ctrl+Shift+X), search `@agentPlugins microsoft-events`, and install |
| Visual Studio 2026 | Copy `skills/microsoft-build/` to a [supported skill location](https://learn.microsoft.com/visualstudio/ide/copilot-agent-skills) |
| Claude Code | `/plugin marketplace add microsoft/Build-CLI` then `/plugin install microsoft-events@microsoft-events-marketplace` |
| Microsoft 365 Copilot Cowork | Download [`microsoft-build-m365.zip`](https://github.com/microsoft/Build-CLI/releases/latest/download/microsoft-build-m365.zip) from the latest release and upload it in Cowork (**Customize** → **Plugins** → **Add plugin** → **Only you**). To build your own offline plugin, see [cli/README.md](cli/README.md#microsoft-365-copilot-plugin-offline) |

## Scope and Limitations

- **Event-scoped:** Targets Build 2026, with Build 2025 and Ignite 2025 also available. The architecture supports any event that uses the same catalog endpoints.
- **Dependency-driven:** Recommendations are only as good as the dependency files in your project root. If your project doesn't have a manifest file, tell the skill your stack directly.
- **Not offline:** Requires network access to query the catalog and Learn MCP server.
- **Not offline:** Requires network access to query the catalog and Learn MCP server. The M365 plugin is the exception: it searches catalog data bundled at build time, which goes stale until you install a newer release or rebuild it.

## Contributing

Expand Down
57 changes: 57 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,14 @@ Use `--event <id>` to filter to a single event. Without it, commands search acro
- **Network-friendly checks**: recent checks are skipped, stable catalogs are checked less often, and failed checks use backoff with jitter to avoid request spikes.
- **Disambiguation**: if a session code exists in multiple events, the CLI shows options.
- **Results**: 10 by default, `--limit` to override.
- **Offline mode**: with `MSEVENTS_OFFLINE=1`, commands read only what's already in the cache. They never check for updates or write to the cache folder, so it can be read-only. Events without a cache are skipped with a note, `refresh` exits with an error, and `status` says it's offline (`"offline": true` on each `--json` entry).

## Environment variables

| Variable | Default | Purpose |
|----------|---------|---------|
| `MSEVENTS_CACHE_DIR` | per-OS cache path | Override the local cache directory. |
| `MSEVENTS_OFFLINE` | unset | Set to `1` or `true` to use only cached data, with no network access or cache writes. |
| `MSEVENTS_FETCH_TIMEOUT_MS` | `30000` | Abort catalog requests after this many milliseconds. |
| `MSEVENTS_MAX_RESPONSE_BYTES` | `52428800` (50 MiB) | Reject catalog responses larger than this. |

Expand All @@ -103,3 +105,58 @@ npm run build
npm test
node dist/index.js --help
```

## Microsoft 365 Copilot plugin (offline)

Microsoft 365 Copilot Cowork runs skill scripts in a sandbox with Node.js but no network access, so `npx -y @microsoft/events-cli` can't run there. `npm run build:m365` produces a Cowork-ready plugin that ships the whole `msevents` CLI as one bundled file, `scripts/msevents.mjs`, together with a frozen snapshot of its cache in `data/`. The bundle always runs in offline mode (`MSEVENTS_OFFLINE`) with its cache folder set to `../data/`, so search results match `msevents` for the same catalog data.

Each [M365 plugin release](https://github.com/microsoft/Build-CLI/releases) (tagged `m365-v<version>`) includes a prebuilt `microsoft-build-m365.zip` (see [Publishing the M365 plugin](#publishing-the-m365-plugin)). To build your own:

```bash
cd cli
npm ci
npm run build:m365 -- --plugin-version 1.0.7 --package
```

| Option | Default | Purpose |
|--------|---------|---------|
| `--plugin-version <x.y[.z]>` | repo plugin version | Plugin version. Cowork rejects versions starting with `0` and requires a higher version for every reinstall. |
| `--package` | off | Also build the installable app package zip with the [Microsoft 365 Agents Toolkit CLI](https://www.npmjs.com/package/@microsoft/m365agentstoolkit-cli) (`npm install -g @microsoft/m365agentstoolkit-cli`). |
| `--event <id>` | all supported events | Event to include; repeat for more than one. |
| `--out <dir>` | `<repo>/out/m365-plugin` | Plugin output folder (git-ignored). `--package` writes the app project to `<out>-app`. Only previous outputs are overwritten. |
| `--sessions-dir <dir>` | download live | Use an existing `msevents` cache folder (`<event>-sessions.json` and `<event>-meta.json`) instead of downloading. |

Output layout:

```text
out/
├── m365-plugin/ # plugin folder
│ ├── .claude-plugin/plugin.json
│ ├── .mcp.json
│ └── skills/microsoft-build/
│ ├── SKILL.md # skills/microsoft-build/SKILL.md, rewritten for offline use
│ ├── scripts/msevents.mjs # the msevents CLI, bundled and locked to offline mode
│ └── data/ # frozen cache: <event>-sessions.json and <event>-meta.json
└── m365-plugin-app/ # with --package: atk project
└── appPackage/build/appPackage.zip
```

The Cowork SKILL.md is built from the repo skill, `skills/microsoft-build/SKILL.md`, so both stay in sync. The repo skill tells the agent to use `npx`, the catalog endpoints and `refresh`, none of which work in Cowork's offline sandbox, so the build:

- replaces the `## Session catalog access` section and the `compatibility`, `metadata` and `allowed-tools` frontmatter with [`cli/m365-skill/offline-overlay.md`](m365-skill/offline-overlay.md), filling in the snapshot date and bundled events;
- rewrites the remaining online steps (`npx -y @microsoft/events-cli` becomes `node scripts/msevents.mjs`, "Without CLI" fallbacks and catalog URLs are removed) with `SKILL_REWRITES` in `scripts/build-m365-plugin.mjs`;
- fails if a rewrite no longer matches the repo skill, if online wording is left, or if the result breaks Cowork's skill validation rules.

When you edit the repo skill, run `npm run smoke:m365`; CI runs it too. The result is about 28,000 characters (roughly 7,000 tokens), above the ~5,000 tokens Cowork recommends for a skill body but within what it accepts.

To install, upload the zip (`appPackage.zip` from a local build, or `microsoft-build-m365.zip` from a release) in Cowork (**Customize** → **Plugins** → **Add plugin** → **Only you**), or sideload it with `atk install --file-path <zip> --scope Personal`. Plugin skills appear only in new conversations.

The snapshot doesn't update itself. Install a newer release, or rebuild with a higher `--plugin-version` and reinstall, to pick up catalog changes. `npm run smoke:m365` builds from a fixture offline, makes the data read-only, and checks that the bundle's output matches `msevents`. With `-- --package` (needs `atk`), it also builds the app package zip, unzips it, checks that its skill files match the plugin folder, and runs the same checks against the unzipped skill; CI runs it this way.

### Publishing the M365 plugin

The [M365 plugin workflow](../.github/workflows/m365-plugin.yml) builds and publishes the plugin. Run **M365 plugin** from the **Actions** tab on `main` and enter a version. It runs `npm run build:m365 -- --package` with live catalog data, then creates a release tagged `m365-v<version>` with `microsoft-build-m365.zip` attached and marks it **Latest**, so [`releases/latest/download/microsoft-build-m365.zip`](https://github.com/microsoft/Build-CLI/releases/latest/download/microsoft-build-m365.zip) serves it.

- **Version:** the M365 plugin is versioned separately from the plugin manifests, because a catalog refresh needs a new release even without code changes. Enter `x.y.z`, higher than the last `m365-v*` release; Cowork only updates an installed plugin to a higher version.
- **Events:** leave the events field empty to bundle every supported event, or enter event IDs separated by spaces, such as `build-2026 ignite-2026`, to bundle only those. Use it when an event's catalog can't be downloaded, because the build fails if any requested event fails.
- **Testing:** the release is public as soon as the workflow finishes. To try changes first, build locally with `npm run build:m365 -- --plugin-version <version> --package` and install the zip.
Loading
Loading