Skip to content

feat: Microsoft Teams frontend via Power Automate - #2

Merged
dylanneve1 merged 14 commits into
mainfrom
feat/teams-v2
Mar 20, 2026
Merged

feat: Microsoft Teams frontend via Power Automate#2
dylanneve1 merged 14 commits into
mainfrom
feat/teams-v2

Conversation

@dylanneve1

Copy link
Copy Markdown
Owner

Summary

  • Microsoft Teams frontend using Power Automate webhooks + Microsoft Graph API
  • Send messages via Adaptive Cards, receive via Graph API chat polling
  • OAuth device code flow (no Azure AD app registration needed)
  • Proper markdown→Adaptive Card conversion using marked lexer
  • Code blocks render as monospace text with preserved whitespace alignment
  • 21 NPUW plugin tools fully working on Teams

Key changes

  • src/frontend/teams/ — 6 files: index, actions, tools, graph, formatting, proxy-fetch
  • prompts/teams.md — Teams-specific formatting rules for Claude
  • Gateway string chatId support (Teams uses non-numeric chat IDs)
  • MCP subprocess tsx resolution fix (absolute path from node_modules)
  • talon chat overrides config.frontend to prevent wrong tools leaking

Fixes included

  • MCP subprocess crash when cwd is ~/.talon/workspace (no node_modules)
  • Teams MCP tools "No active chat context" (string→numeric chatId mapping)
  • Code blocks in Adaptive Cards (v1.4 Container + monospace + non-breaking spaces)
  • Inline backtick stripping (Teams TextBlock doesn't support them)
  • Frontend tool leak in terminal mode when config says teams

Test plan

  • 24 Teams-specific tests (formatting, actions, proxy, graph exports)
  • 629 total tests passing
  • Manual testing: plain text, markdown, code blocks, tables, buttons
  • Jenkins tools working end-to-end on Teams

🤖 Generated with Claude Code

dylanneve1 and others added 14 commits March 20, 2026 13:12
Bidirectional Teams channel integration without Azure AD or Bot Framework.
Send direction POSTs Adaptive Cards to a Power Automate workflow webhook URL.
Receive direction accepts POSTs from a Power Automate flow on a dedicated HTTP server.

New files:
- src/frontend/teams/ (index, actions, formatting)
- prompts/teams.md

Modified: config schema, index.ts, cli.ts, log.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- formatting: buildAdaptiveCard (text, URL buttons, Submit buttons, empty)
- formatting: splitTeamsMessage (paragraph split, line split, hard split)
- formatting: stripHtml (tags, entities, nested, Teams mentions)
- actions: send_message (success, empty no-op, webhook failure)
- actions: get_chat_info, unsupported actions (graceful no-ops), unknown
- proxy-fetch: exports verification
- graph: exports verification

629 tests total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: `--import tsx` resolves tsx from cwd, not the script directory.
After moving workspace to ~/.talon/workspace/ (which has no node_modules),
all MCP subprocesses failed to start silently. Tools appeared missing.

Fix: resolve tsx to its absolute path from Talon's own node_modules:
  node_modules/tsx/dist/esm/index.mjs

Affects: plugin MCP servers, telegram-tools, teams-tools — all fixed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…elegram

talon chat always uses terminal frontend, but config.frontend wasn't
overridden — backend spawned teams-tools MCP server, and system prompt
loaded teams.md instead of terminal.md. Claude saw send_message tool
that doesn't work in terminal.

Fix: override config.frontend to "terminal" and rebuild system prompt
before backend initialization in talon chat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/backend/claude-sdk/ is 3 levels deep from project root, not 2.
../../node_modules/ resolved to src/node_modules/ (doesn't exist).
Teams MCP tools failed to spawn because of this.

Terminal worked because plugin.ts (src/core/) is only 2 levels deep.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: Teams uses string chat IDs like "teams_chat_19:6d5c40..."
which get passed as TALON_CHAT_ID to MCP subprocesses. The gateway
tried Number("teams_chat_19:...") → NaN → context not found.

Fix: gateway.setContext() now stores an optional stringId alongside
the numeric key. handleAction() tries numeric parse first, then
falls back to string ID lookup across active contexts.

The dispatcher passes both numericChatId and string chatId to
context.acquire() so the mapping is established.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adaptive Card TextBlock doesn't support fenced code blocks (```).
Messages with code blocks rendered as "Card unsupported" errors.

Fix: parse fenced code blocks out of the message text and render them
as CodeBlock elements (Adaptive Cards v1.6). Prose stays as TextBlock.
Messages with mixed code and text get split into alternating elements.

Schema version bumped from 1.4 to 1.6 (required for CodeBlock support).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CodeBlock element requires schema v1.6 which Power Automate webhooks
don't support ("card couldn't be displayed" error).

Reverted to schema v1.4 with a workaround: fenced code blocks are
parsed out and rendered as monospace TextBlocks inside grey Containers
(style="emphasis"). Not as pretty as native code blocks but renders
correctly in all Teams clients.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Teams Adaptive Card TextBlock supports **bold**, _italic_, [links] but
NOT backtick inline code. Inline backticks caused broken rendering.

Now strips `code` → code in TextBlock prose. Fenced code blocks still
render as monospace Containers (confirmed working in Test 3).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d not headings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced regex-based code block parsing with marked's AST tokenizer.
Each markdown token type maps to the right Adaptive Card element:

  paragraph  → TextBlock (bold/italic preserved, inline backticks stripped)
  heading    → bold TextBlock with Medium size
  code block → monospace TextBlock in emphasis Container (grey box)
  list       → TextBlock with bullet/number prefixes
  blockquote → emphasis Container with subtle text
  hr         → subtle line

No regex hacks — the marked lexer handles all markdown parsing.
Inline backticks stripped via cleanInline() since Teams doesn't support them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TextBlock with wrap:true collapses newlines into spaces. Now each
line of a code block is a separate TextBlock with spacing:None
inside the emphasis Container, preserving the original formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Teams TextBlock collapses consecutive spaces. Replace with
non-breaking spaces (U+00A0) in code block lines to preserve
column alignment in tables and formatted output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dylanneve1
dylanneve1 merged commit c81cde9 into main Mar 20, 2026
1 check passed
dylanneve1 added a commit that referenced this pull request Apr 10, 2026
- Validate mcpServer.args elements are strings and reject empty command (#1)
- Shell-quote interpolated paths in dream bash commands (#2, #11)
- Replace CLI-based diary write with mempalace_diary_write MCP tool (#3)
- Make validation error message platform-agnostic (#4)
- Update mempalacePython comment for platform-dependent default (#5)
- Wrap mp.init() in Promise.race with 30s timeout (#6)
- Make init conditional on successful validation, pass actual config (#7)
- Move import mempalace check into validateConfig (#8)
- Replace execFileSync with async execFile in init() (#9)
- Document that registerPlugin does NOT call init (#10)
- Update dream prompt header from "4-stage" to "5-stage" (#12)
- Update getPluginMcpServers JSDoc to document mcpServer path (#13, #17)
- Add .min(1) to palacePath/pythonPath zod schemas (#14)
- Distinguish ENOENT/EACCES/EPERM from import failures in validation (#15)
- Fix test name from "logs warning" to match actual behavior (#16)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dylanneve1 added a commit that referenced this pull request Apr 10, 2026
* feat: integrate mempalace as built-in plugin for long-term memory

Adds mempalace (Python MCP server) as a first-class memory system.
When enabled, the agent gets semantic search, knowledge graph, and
verbatim memory storage via ChromaDB — all local, zero API calls.

Key changes:
- Extend plugin system with `mcpServer` field for non-Node MCP servers
- Add `registerPlugin()` for built-in plugin registration
- Create mempalace plugin (factory pattern, validates python venv)
- Wire mempalace into dream mode (Stage 5: mine logs into palace)
- Add `mempalace` config schema (enabled, palacePath, pythonPath)
- Add default paths for palace dir and python venv binary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(dream): mine daily notes instead of raw logs, add diary writing

Dream Stage 5 now mines memory/daily/ (curated observations) instead of
raw logs/ directory, eliminating junk chunks (tool JSON, df output, etc).
Added personal diary writing instruction — agent reflects on feelings,
state of mind, learnings, and loose threads after each dream run.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR #27 review comments

- plugin.ts: validate mcpServer.args entries are strings and reject empty command
- dream.ts: quote interpolated paths in shell commands, replace mcp_server CLI diary with direct file write
- mempalace/index.ts: platform-agnostic error message for missing python binary
- paths.ts: update comment to reflect platform-dependent venv path
- bootstrap.ts: wrap mempalace init in 30s timeout to match loadSinglePlugin behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restore mempalace CLI diary writer, keep path quoting

Copilot suggested removing the mcp_server CLI invocation for diary
writing but that's the intended mempalace interface. Restored it
with quoted paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add gating/validation tests for mempalace integration

- plugin.ts: test rejection of empty mcpServer.command and non-string args elements
- dream.ts: test mempalace section gating — verify mining/diary instructions
  only appear when mempalace is configured, skip message when not
- 1306 tests passing (4 new)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: upgrade mempalace system prompt with comprehensive tool docs

Adapted from mempalace SKILL.md (v3.1.0). Key improvements:
- Session protocol (verify before responding, invalidate stale facts)
- Full tool documentation including kg_timeline, traverse, find_tunnels,
  diary_read/write, delete_drawer, graph_stats, check_duplicate
- Semantic search tips (meaning-based, not keyword)
- Knowledge graph temporal validity guidance
- Tests updated to verify all tool names appear in prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract mempalace prompt to prompts/mempalace.md

Move system prompt instructions out of TypeScript into a .md file,
matching the pattern used by dream.md and other prompts. Plugin loads
and interpolates {{palacePath}} at runtime with graceful fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace pixi.intel.com registry URLs in lockfile with npmjs.org

Lockfile had resolved URLs pointing to pixi.intel.com (private/corporate
registry) for @Anthropic-AI packages, causing CI to fail with ENOTFOUND.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve all lint warnings and formatting issues

Remove unused imports, variables, and catch bindings across 14 files.
Add yield statements to generator function mocks. Fix prettier formatting.

0 lint warnings, 0 format issues, 1307 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address all 17 Copilot review comments on PR #27

- Validate mcpServer.args elements are strings and reject empty command (#1)
- Shell-quote interpolated paths in dream bash commands (#2, #11)
- Replace CLI-based diary write with mempalace_diary_write MCP tool (#3)
- Make validation error message platform-agnostic (#4)
- Update mempalacePython comment for platform-dependent default (#5)
- Wrap mp.init() in Promise.race with 30s timeout (#6)
- Make init conditional on successful validation, pass actual config (#7)
- Move import mempalace check into validateConfig (#8)
- Replace execFileSync with async execFile in init() (#9)
- Document that registerPlugin does NOT call init (#10)
- Update dream prompt header from "4-stage" to "5-stage" (#12)
- Update getPluginMcpServers JSDoc to document mcpServer path (#13, #17)
- Add .min(1) to palacePath/pythonPath zod schemas (#14)
- Distinguish ENOENT/EACCES/EPERM from import failures in validation (#15)
- Fix test name from "logs warning" to match actual behavior (#16)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add 'mempalace' to LogComponent type

TypeScript type check was failing because 'mempalace' wasn't in the
LogComponent union type used by log/logError/logWarn functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: allow MCP tools in dream prompt when required by Stage 5

Update tool access statement to permit MCP tools for mempalace
mining stage instead of blanket-blocking all MCP tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: duplicate guard in registerPlugin, pass MCP servers to dream

- registerPlugin now checks for duplicates before setting env vars
  or logging success, preventing misleading logs and env clobbering
- Dream agent now receives mempalace MCP servers when configured,
  so Stage 5 diary/mining tools actually work

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add selective MCP server loading via 'only' filter

getPluginMcpServers now accepts an optional plugin name filter:
- omitted = all plugins (backwards compatible for chat sessions)
- [] = none
- ["mempalace"] = only mempalace

Dream mode uses ["mempalace"] to load only what it needs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: load mempalace prompt from dirs.prompts, fix dream systemPrompt

- Mempalace prompt now loads from ~/.talon/prompts/mempalace.md
  (user-customisable, seeded on first run) instead of relative to
  source file. Consistent with heartbeat/dream prompt loading.
- Dream systemPrompt now permits MemPalace MCP tools when configured,
  preventing conflict with the markdown prompt's Stage 5 instructions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: move duplicate check before validation, gate dream on plugin registration

- registerPlugin checks for duplicates before running validateConfig,
  avoiding expensive re-validation on accidental double registration
- Dream mempalace integration now gated on getPlugin("mempalace")
  instead of just config.mempalace.enabled, so failed validation
  or registration doesn't cause dream-time MCP tool failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(mempalace): correct CLI arg order for status check

The --palace flag is a global option that must come before the
subcommand. Wrong order caused the init health check to always
fail with exit 2, logging a misleading "not yet initialized" warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review round 8 — validation, error handling, unused param

- Validate `import mempalace.mcp_server` (actual spawned module) instead of
  just `import mempalace` in validateConfig
- Add timeout/killed error branching in validateConfig catch block
  (ETIMEDOUT, signal, killed) with specific messages instead of generic
  "not installed"
- Include stderr details in import failure messages for debugging
- Remove unused `config` from ProcessAndReplyParams and all processAndReply
  call sites (flushQueue, retry, callback handler)
- Replace `mempalace status` CLI smoke test in init() with a simple import
  check — fixes false "Palace not yet initialized" warning when palace IS
  initialized but CLI subcommand doesn't exist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove config from message queue chain, fix pythonPath comment

- Remove config from queue entry type, enqueueMessage signature, and
  all 3 call sites — completes the cleanup started in round 8
- Eliminates unnecessary TalonConfig reference (including botToken) from
  queue state
- Update mempalace plugin header comment to document platform-dependent
  pythonPath default (bin/python on Unix, Scripts/python.exe on Windows)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
dylanneve1 pushed a commit that referenced this pull request May 7, 2026
* chore(deps): bump hono in lockfile to close CVE alert

Closes Dependabot alert #2 (hono <4.12.14, HTML injection in JSX SSR).

The lockfile was pinning hono@4.12.12 even though node_modules already had
4.12.16 installed (verified via `npm ls hono`). GitHub reads the lockfile,
so the alert kept firing despite the actually-installed version being
unaffected.

`npm update hono @hono/node-server --include=optional` resolves both to
the latest version satisfying the existing transitive constraints
(`^4.11.4`, `^4`). Followed by clean install to ensure node_modules
matches.

- hono: 4.12.12 -> 4.12.16
- @hono/node-server: 1.19.13 -> 1.19.14
- emnapi optional-dep placement reshuffle (npm dedupe, no behavior change)

Tests: 1635/1635 passed. tsc clean. lint 0 errors.

Note: leaves @anthropic-ai/sdk Dependabot alert #3 untouched -- that one
needs an `overrides` block or upstream SDK bump and was flagged for live
review with Dylan.

* fix(deps): surgical lockfile bump (only hono entries) to fix CI

The previous npm install --include=optional regenerated the entire
optional-dep tree, removing @emnapi/core@1.10.0 entries that CI's
plain 'npm ci' (no --include=optional) needs. Reverting to main's
lockfile and only patching the hono and @hono/node-server entries
keeps the diff surgical and cross-platform.

Verified: 'npm ci' (no flags), 1635/1635 tests, tsc clean, 0 lint errors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant