Derive OUT from __dirname (../../../dist) so `node src/build.js` and `pnpm
build` both land bundles at the repo-root dist/ regardless of invocation cwd.
install-linux.sh consumes dist/{firefox,chrome} at that fixed path — the
permanent non-snap dev install (Firefox ESR force-install + Brave
--load-extension) points at it.
aarch64 has no Google-Chrome build and apt ships only snap redirects for
firefox/chromium, so the reliable non-snap browsers are Firefox ESR (Mozilla
arm64 tarball) and Brave (arm64 apt). Firefox ESR gets an enterprise policy that
force-installs the unsigned XPI into every profile with signature enforcement
disabled — permanent + launch-independent. Brave loads the live dist/chrome via
a --load-extension launcher + desktop entry (rebuild → relaunch = new code, the
correct dev loop). Verified: FF addon active (unsigned), Brave MV3 SW live.
Firefox no longer dials the dead ws://:9223/cdp bridge by default (it
error-looped); it's opt-in via globalThis.HANZO_LEGACY_CDP, matching
Chrome's native-ZAP-only default. And the ZAP native-host reconnect goes
3s -> 30s: with no MCP client there's no router, so hammering (and
re-launching the host) every 3s was pure noise — a live consumer is still
picked up within one interval. Pairs with zapd host connect-only.
npm rejects the unscoped 'hanzo-ai' name (too similar to existing 'hanzoai'), and
both publish to their OWN stores (Raycast Store / VS Marketplace) via dedicated
jobs — they don't belong on npm. Setting private so pnpm -r publish skips them.
The recursive publish 404'd on every package (npm's obscured 'unauthenticated'):
setup-node's ${NODE_AUTH_TOKEN}-templated .npmrc is read by npm but not reliably
by pnpm publish. Write the token literally into .npmrc + set @hanzo:registry, and
add npm whoami as an auth diagnostic. .npmrc gitignored.
Normalize every packages/* package for public npm publish:
- version: pre-1.0 (0.x) → 1.0.0; already-≥1.0 kept (mcp 2.4.1,
cli-tools 1.8.0, dxt 1.8.1, browser/hanzo-ide/office/outlook 1.9.31,
gworkspace 1.9.30, aci/auth 1.0.0)
- remove "private" from all 22 gated packages/* (apps/site + repo root
stay private — site is a deployed website, not an npm library)
- add publishConfig.access=public everywhere (scoped @hanzo/* need it)
- add files[] + valid main pointing at built output so no empty tarballs;
no-build packages ship src+README
CI npm job now publishes the whole workspace:
- build @hanzo/cards first (slack/teams import its dist), then
pnpm -r --if-present run build || true
- pnpm -r publish --access public --no-git-checks --ignore-scripts
--ignore-scripts prevents raycast/vscode `publish` lifecycle scripts
(Raycast Store / VS Marketplace) from hijacking and aborting the run.
workspace:* (auth, cards) is rewritten to the real version at publish.
iManage Work OAuth2 + Work API (workspaces/documents/search) + content extraction,
AI actions (summarize doc / extract clauses-dates-parties / search-synthesize);
server-side tokens, never logs content, grounded extraction, 'not legal advice'.
121 tests. On published @hanzo/ai@0.2.0.
All 6 Wave 5 verticals now on main: epic(109) procore(102) quickbooks(91)
canvas-lms(119) workday(113) imanage(121). CI step added.
New package @hanzo/imanage: an embedded-app panel over @hanzo/ai for iManage
Work (the dominant legal document management system), mirroring the
packages/procore OAuth + API-proxy + pure-core shape.
- OAuth2 Authorization Code against the iManage Control Center
(/auth/oauth2/authorize + /token); server-side token exchange + refresh, the
client secret never reaches the browser.
- Work API v2 wrappers (workspaces, folder children, document profile +
content, search, gated profile write-back) scoped by customer + library in
the path, X-Auth-Token auth, envelope/offset-limit pagination. Pure.
- Pure legal document-context assembly with text extraction (binary/OCR out of
scope, reported honestly) and one truncation-honest windowing algorithm.
- Four AI actions — summarize document, extract key clauses/dates/parties,
search-and-synthesize a matter, compare a document set — plus freeform ask,
over a single grounded ask() path.
- Same-origin proxy keeps tokens + secret server-side; NEVER logs document
content. Legal confidentiality posture documented in the README.
- 121 vitest tests (config/oauth/api/parse/documents/hanzo/actions/panel),
tsc --noEmit clean, node build.js green.
Registers packages/imanage in pnpm-workspace.yaml (one line).
@hanzo/workday: an embedded-app panel + read-only OAuth + API-proxy service
over Workday people data, built on @hanzo/ai + @hanzo/iam via api.hanzo.ai.
Mirrors packages/procore's pure-core shape.
- OAuth2 Authorization Code + refresh against the tenant token endpoint
(…/ccx/oauth2/{tenant}/token) with HTTP Basic client auth (secret in the
header, never the body); non-rotating refresh carried forward.
- Workday REST wrappers (staffing v6 workers/orgs/directReports, recruiting v4
requisitions, absenceManagement v1 absence types) with limit/offset + the
{ total, data } envelope, plus RaaS custom-report reads ({ Report_Entry }).
- Pure people-context assembly with honest budget/truncation windowing.
- Five HR-guardrailed AI actions: summarize worker, draft job description,
org & headcount, answer HR question, draft review feedback.
- Read-only by design: the proxy is GET-only (405 otherwise); write-back is
documented as a gated future addition.
- 113 vitest tests; tsc --noEmit clean; esbuild build green.
Registers packages/workday in pnpm-workspace.yaml.
Read+assist clinical copilot embedded in the EHR via SMART on FHIR. Loads the
launched patient's chart over FHIR R4 (problems, meds, allergies, labs/vitals,
notes) and runs four actions — summarize patient, draft SOAP note, extract
problems & meds, ask — through the api.hanzo.ai gateway via @hanzo/ai@^0.2.0.
PHI posture: the FHIR/PHI access token is confined server-side (server.ts);
the browser holds only an opaque HttpOnly session cookie and a Hanzo gateway
key. No PHI is ever logged; SMART is auth-code + PKCE (S256) with the
confidential secret read from the environment; the proxy is scoped to the six
read resources and the session's own FHIR base (SSRF/cross-tenant guards).
Read + assist only — no clinical write-back, no *.write scope.
Tests: 109 vitest across config/smart-oauth/fhir-client/chart/hanzo/auth/server.
Workspace-wired like every sibling: test/ dir, root pnpm-lock importer, no
package-level lock.
Windows built both installers (.msi 1.82MB + .exe 1.27MB) but upload-artifact's
path: /tmp/desktop/* didn't match Git-Bash's /tmp on windows-latest → 'No files
were found'. macOS/Linux worked (consistent /tmp). Use a workspace-relative
desktop-artifacts/ dir — identical resolution for the bash shell and the upload
action on all three OSes.
Adds packages/procore (@hanzo/procore): an embedded-app panel + OAuth/API-proxy
service that brings Hanzo AI to Procore construction projects.
- OAuth2 Authorization-Code (login[-sandbox].procore.com) with server-side
secret + rotating refresh; tokens never reach the browser.
- Pure Procore REST v1.0 wrappers (projects, RFIs, submittals, documents,
observations, daily logs) with Procore-Company-Id scoping + pagination.
- Pure project-context assembly with honest truncation.
- AI actions over @hanzo/ai (imported, not reimplemented): summarize RFI,
draft RFI response, extract action items/risks, project status, plus ask.
- Optional explicit RFI reply write-back through the same proxy.
- 102 vitest tests; tsc --noEmit clean; esbuild build green.
- Registers packages/procore in pnpm-workspace.yaml.
Two build bugs (16 other v1.9.31 assets shipped fine; these were the last two):
- desktop: register_hotkey returned tauri::Result, but global_shortcut register()
yields global_shortcut::Error with no From into tauri::Error (E0277). Return
Box<dyn Error> — both ? sites convert, and setup() already expects it.
- jupyter: hatch-jupyter-builder ran 'jlpm install' (yarn) at the pnpm monorepo
root → rejected. Build the prod labextension via pnpm first (creates the
skip-if-exists target), so the wheel build never invokes jlpm.
The 'Build labextension' step calls jlpm (provided by jupyterlab), but jupyterlab
was only installed in the later wheel step → 'jlpm: not found' failed the job, and
since jupyter is in release.needs it SKIPPED the whole GitHub Release (15 good
artifacts, no release). Install the JupyterLab toolchain first + continue-on-error
on every jupyter step so a wheel failure can never block the release again.
Add producer jobs mirroring the office/outlook pattern so a version tag
attaches downloadable artifacts for every surface that can be one:
- figma/sketch/zendesk: pnpm build -> bestzip dist/ (arc runner has no zip)
- teams: build + script -> dist/hanzo-teams.zip (nil-GUID app id
placeholder so the release asset always builds without a bot secret)
- desktop: matrix [macos, windows, ubuntu] Tauri build -> per-OS installer
(.dmg / .msi+.exe / .AppImage+.deb), continue-on-error (signing is future)
- jupyter: build labextension -> python -m build --wheel
Wire all into the release job needs/files and extend the download table
with Design Tools / Team Apps / Desktop / Data Science sections. Every
advertised link resolves to a produced basename.
The extension's packages/ai was a duplicate 'AgentKit with MCP' (canonical home:
hanzoai/ai) that squatted the @hanzo/ai name and collided with the published AI
client. Nothing consumes it anymore (all 16 adapters resolve @hanzo/ai to the
published 0.2.0 client from npm; no @hanzo/ai/server|react subpath imports; not
in the release publish). Removing it makes @hanzo/ai in the workspace
unambiguously the AI client.
Taxonomy (per direction): @hanzo/ai = AI client · @hanzo/agent = Agent SDK ·
@hanzo/ui = all v8 components (hanzoai/ui, agent UI → @hanzo/ui/agent) ·
@hanzo/gui = v8 framework (hanzoai/gui, Tauri+Tamagui unified). Removed the
@hanzo/ai CI test step. github 84 + slack 68 still green.
5 early adapters (docusign/meetings/notion/slack/teams) pinned @hanzo/ai@^0.1.1,
which resolved to the LOCAL packages/ai AgentKit (no createAiClient) — the reason
they carry stand-in client.ts files. Bump them to ^0.2.0 so every adapter's
declared dep points at the real published headless client. Tests unchanged and
green (docusign 103, meetings 70, notion 104, slack 68, teams 69).
Remaining SDK cleanup (separate pass): rename the extension's own @hanzo/ai
AgentKit → @hanzo/agentkit to end the name collision, then replace each adapter's
stand-in client.ts with a one-line re-export of @hanzo/ai.
A cross-platform (macOS/Windows/Linux) menubar/tray app with a global hotkey
(Cmd/Ctrl+Shift+Space) that pops a Hanzo assistant over any app. Ask, summarize
the clipboard, rewrite, explain, translate, fix grammar — streamed over
api.hanzo.ai via the published @hanzo/ai. Built on Tauri v2 (Rust shell + web
frontend), not Electron.
Reuses the raycast/office design one-to-one: pure host-agnostic action catalog
(actions.ts — prompts, clipboard-context assembly/truncation, message shaping,
reply parsing), a thin @hanzo/ai wrapper (hanzo.ts — run/stream/list), a local
settings store (store.ts — hk- key + model, parse/merge/serialize), and the only
Tauri-touching module (clipboard.ts). app.ts is glue with one run path serving the
panel, the tray quick actions, and the hotkey.
Rust shell (src-tauri): tray + menu, global-shortcut toggle, spotlight window
(small, always-on-top, hidden-on-blur), and hide_window/paste commands (enigo
synthesizes Cmd/Ctrl+V so rewrite/translate/fix land over the frontmost app).
The hk- key lives only in the webview's local store and is validated before use;
CSP allows connecting only to api.hanzo.ai. Frontend builds with esbuild + tsc
clean; 66 vitest tests green. Native installers (.dmg/.msi/.AppImage) are a
per-OS CI job (tauri build) — never built locally.
Registers packages/desktop in pnpm-workspace.yaml.
@hanzo/jupyter: a JupyterLab 4 labextension. A side-panel assistant (model
picker + prompt + streaming over @hanzo/ai) that reads active-cell / selection
/ whole-notebook context, plus cell actions (explain, fix from traceback,
document, optimize, generate-a-cell). Model calls route through api.hanzo.ai
/v1 via the published @hanzo/ai createAiClient; the hk- key lives in the
JupyterLab settings registry (schema/plugin.json).
Pure core (notebook.ts context/traceback/truncation, hanzo.ts action catalog +
request shaping) is unit-tested with vitest (58 tests); the widget layer
(index.ts plugin, panel.tsx) is the thin imperative shell. tsc --noEmit clean.
Ships a hatch-jupyter-builder pyproject for pip install / PyPI.
- @hanzo/github — GitHub App: auto PR review (diff windowed), issue triage
(labels intersected with real repo labels), @hanzo mentions; HMAC-verified
webhooks; all via @hanzo/ai (84 tests).
- hanzo-ai (Raycast) — command bar: Ask (streaming) + Summarize/Explain/Rewrite/
Translate/Fix-grammar over selection/clipboard, paste-back (51 tests).
Both on published @hanzo/ai@0.2.0 + @hanzo/iam. CI steps added.
New @hanzo/raycast package (Raycast manifest name: hanzo-ai): the Hanzo
command bar. One view command (Ask Hanzo — Form → streaming Detail) and
five no-view quick actions (Summarize, Explain, Rewrite, Translate, Fix
Grammar) that read the frontmost app's selection (clipboard fallback),
run it through the published @hanzo/ai over api.hanzo.ai /v1, and paste
or copy the result back.
Pure core, thin adapters (mirrors @hanzo/canva + @hanzo/figma):
- config.ts gateway URLs, default model (zen5), hk- key guard
- actions.ts action catalog, prompts, context assembly + truncation,
capture policy (chooseSubject), response parsing
- hanzo.ts the only caller of @hanzo/ai: runChat / streamChat / listModels
- selection.ts the only caller of @raycast/api I/O
- run-action.ts shared quick-action runner; per-command one-liners
- ask.tsx the streaming view command
Auth: the hk- key is a secure Raycast preference (never committed).
51 vitest tests (catalog, prompts, context, request shaping incl.
streaming, parsing, capture policy). tsc --noEmit clean; ray build
succeeds; ray lint clean (ESLint 9 flat config + Prettier).
Registers packages/raycast in pnpm-workspace.yaml.
A side-panel Canva app over the published @hanzo/ai: generate copy, rewrite
the selected text in place, translate, brainstorm content ideas, and ask.
- design-core.ts (PURE): the five-action catalog, prompt builders, design-
context assembly + budget truncation, and response parsing (fence/quote/
list-marker stripping). Mirrors @hanzo/figma's design-core. Fully unit-tested.
- hanzo.ts: thin over @hanzo/ai (createAiClient) — the only model-gateway path.
- canva.ts: thin over @canva/design — observe the plaintext selection, replace
it via a fresh draft, insert a new text element at the cursor.
- app.tsx: the @canva/app-ui-kit panel (model picker, action picker, output,
ideas). index.tsx mounts it under AppUiProvider.
- config.ts: api.hanzo.ai /v1 gateway, default zen5, hk- key guard.
40 vitest tests green, tsc --noEmit clean, esbuild build succeeds. Registered
in pnpm-workspace.yaml.
New @hanzo/figma package: a Figma + FigJam plugin that rewrites, translates,
content-fills, critiques (with a11y notes), renames layers, and generates copy
variants for the selected layers — built on the published @hanzo/ai headless
client against api.hanzo.ai.
Two-thread architecture per the Figma plugin model:
- code.ts (main): serializes the selection into a plain SelectionSnapshot
(text nodes + structure + colors, node-budgeted) and applies write-backs
(font-loaded setCharacters, rename, createText) — full figma.* access, no net.
- ui.ts (iframe): the only place the model is called; streams via design-core,
parses per action, posts edits/inserts back to code.ts over a typed
postMessage protocol (messaging.ts).
- design-core.ts (PURE): the design-action catalog (system+user prompt builders
per action), selection->context assembly + budget truncation, response parsing
(JSON id->value edit maps with id allow-listing, variant-list splitting), and
the @hanzo/ai run funnels over an injectable client.
Key pasted in the UI, validated via /v1/models, stored in figma.clientStorage;
manifest networkAccess allowlists only api.hanzo.ai. esbuild build emits the
three Figma files (code.js IIFE, ui.html with JS+CSS inlined, manifest.json).
56 vitest tests green (design-core + messaging guards); tsc --noEmit clean.
A HubSpot public app on the developer-projects platform: a CRM card UI
extension (crm.record.tab, on contacts/companies/deals) backed by two
serverless functions. Summarize a record, draft an email, suggest next
steps, or ask — grounded in the record + its associated records — and
write the result back as a Note or Task.
Built on the published @hanzo/ai@^0.2.0 headless client (createAiClient,
/v1 chat.completions + models.list) + @hanzo/iam. The Hanzo API key
(HANZO_API_KEY secret) and the per-portal OAuth token live only in the
serverless backend; the browser never sees them.
Pure, unit-tested core (config, hanzo call + context windowing + the four
actions, HubSpot CRM v3 request shaping/parsing, OAuth token exchange,
record→context assembly, Note/Task write-back payloads). 75 vitest tests
green, tsc --noEmit clean, dual ESM+CJS build so the CJS serverless
runtime can require it.
Add packages/shopify (@hanzo/shopify): a Shopify embedded admin app that puts
Hanzo AI over the store as system of record.
- Product content: generate/rewrite descriptions + SEO title/meta from product
attributes, then write back via the productUpdate mutation (only changed fields).
- Orders/support: summarize an order, draft a customer reply, extract issues.
- Ask: freeform question over a product or order, grounded in its data.
Server (src/server, holds the only secrets):
- oauth.ts Shopify OAuth request shaping + OAuth-callback HMAC verify (hex over
Shopify-canonical URLSearchParams form) + state (CSRF) check.
- shopify-api.ts Admin GraphQL wrappers (getProduct/getOrder/updateProduct) —
pure request shaping + parsing + productUpdate userErrors.
- hanzo.ts thin over the PUBLISHED @hanzo/ai createAiClient; product/order
context assembly + prompt building. No transport reimplemented.
- actions.ts product + order AI action catalogs, one prompt each over ask.
- webhooks.ts webhook HMAC verify (base64 over RAW body) + topic routing
(orders/create, app/uninstalled, the 3 GDPR topics).
- server.ts OAuth + /v1/* proxy the frontend calls + /webhooks. HMAC verified
before any param/body is trusted; shop validated to *.myshopify.com.
Frontend (src/app): Polaris + App Bridge React panel — api.ts (proxy client,
no secrets), context.ts (launch context → GID), session-fetch.ts (App Bridge
session-token fetch), App/Assistant/main. App Bridge loaded from Shopify CDN;
public API key stamped into the app, secret never bundled.
Plus shopify.app.toml, an admin-link extension, build.js (esbuild: app bundle +
Node server), tsconfig, vitest config, README. 108 vitest tests (pure), tsc
--noEmit clean, build green. Model calls via @hanzo/ai to api.hanzo.ai/v1 only.
Register packages/shopify in pnpm-workspace.yaml.
ZAFv2 ticket_sidebar Support app on the published @hanzo/ai@0.2.0: summarize
ticket, draft reply (agent voice), suggest macro/next-steps, ask; write-back via
ticket.comment.appendText (public reply / internal note). Hanzo key is a secure
ZAF setting, proxied server-side so it never reaches the browser (49 tests). CI step added.
ZAFv2 ticket_sidebar app (@hanzo/zendesk). Reads the current ticket +
conversation via the ZAF client, offers Summarize / Draft reply / Suggest
next steps / Explain / Ask over the published @hanzo/ai headless client, and
writes results back into the reply editor or as an internal note.
The Hanzo API key is a SECURE installation setting: model traffic is proxied
through Zendesk's request API (secureFetch + {{setting.hanzo_api_key}}), so the
key never reaches the browser while still using createAiClient. Pure, tested
ticket-windowing (newest-first, honest scope note) and message shaping.
49 vitest tests, tsc --noEmit clean, esbuild -> dist/ ZAF bundle (zcli-ready).
Notion has no in-app UI-extension surface, so this ships a public OAuth
integration + REST v1 client and a small web app that operates on the
user's pages/databases. Four actions over a page:
- Summarize: recursively read blocks -> text, summarize via @hanzo/ai,
append as a callout.
- Draft/expand: generate content continuing the page.
- Extract -> database: pull {task, owner, due} action items and create
one database row per item.
- Ask: freeform Q&A over page context.
Architecture mirrors @hanzo/clio: pure, unit-tested request/blocks/chat
modules with thin DOM + http glue. The Notion client_secret AND bot token
stay server-side (server.ts) — the browser holds only an opaque session id
and proxies every Notion call through an allow-listed /notion/* proxy.
Model calls route through the createAiClient contract to api.hanzo.ai/v1.
104 vitest tests (OAuth shaping, REST wrappers + pagination, blocks<->text
both directions, @hanzo/ai request shaping, context assembly/truncation,
action-item parsing, proxy allow-list). tsc --noEmit clean; esbuild builds.
New @hanzo/salesforce SFDX package: a Lightning Web Component assistant on
Account/Opportunity/Case/Lead/Contact record pages, backed by Apex callouts to
the api.hanzo.ai/v1 gateway (OpenAI-compatible, same wire format as the browser
extension, Office add-in, gworkspace add-on, and Slack app).
- HanzoClient: pure request/response core (buildMessages/requestBody/
extractContent/parseModels) + ask/listModels callouts via the Hanzo_API
Named Credential — the key never touches Apex.
- HanzoContext: SOQL record + related-list context assembly per object,
WITH SECURITY_ENFORCED, truncated to a token budget.
- HanzoController: @AuraEnabled complete/listModels/writeBack; write-back
creates a Task, a ContentNote, or a Chatter FeedItem (as user).
- LWC hanzoAssistant: model picker + quick actions (Summarize, Draft email,
Next steps, Risks) + prompt + write-back, on record pages.
- Named + External Credential for api.hanzo.ai, permission set with principal
access, Connected App, Hanzo_Config__mdt default-model config.
- Tests: HanzoClientTest + HanzoControllerTest with HttpCalloutMock (asserts
URL /v1/chat/completions, bearer, body model+messages, response parse, and
each write-back) — >75% coverage; vitest over the truncate helper.
- Registered packages/salesforce in pnpm-workspace.yaml.
@hanzo/teams — bot (1:1/channel) + message extensions, Adaptive Cards via
@hanzo/cards, Action.Submit/Execute dispatch, packaged Teams app zip (69 tests).
CI: add teams test step. Wave 1 (Slack · Teams · Zoom/Meet) all on main.
Wave 1 comms hubs land:
- @hanzo/slack — /hanzo slash command, App Home, message shortcuts, thread-context
quick actions, OAuth install; UI 100% @hanzo/cards Block Kit (68 tests).
- @hanzo/meetings — Zoom App + Google Meet add-on panels + Zoom webhook
transcript→summary (signature-verified); web panels over @hanzo/ai (70 tests).
CI: build @hanzo/cards before dependent tests (consumers resolve its built dist/),
and add slack + meetings test steps.
Note: the headless @hanzo/ai client isn't published under 'latest' yet (npm
@hanzo/ai@0.1.1 is the React <HanzoAI/> UI); each adapter ships a client to the
exact createAiClient /v1 contract as an interim, to become a one-line re-export
once the headless client publishes.
A Bot Framework (botbuilder) app bringing Hanzo AI into Microsoft Teams,
built on the shared foundation — @hanzo/ai (headless client), @hanzo/iam
(identity), @hanzo/cards (canonical PanelSpec -> Adaptive Card).
- Bot (TeamsActivityHandler): 1:1 + channel chat; @mention/DM a prompt ->
completion -> reply as a Hanzo assistant Adaptive Card panel.
- Adaptive Card actions: model picker (Input.ChoiceSet), quick actions
(Draft/Summarize/Explain/Extract action items), prompt input + submit,
all routed by dispatch() on data.action (from @hanzo/cards).
- Message extension "Ask Hanzo" (action) on a message + "Compose with
Hanzo" (query) to insert an AI draft; pulls message/thread context.
- Teams app manifest (schema v1.16) + color/outline icons + a dependency-
free zip build step for the sideloadable app package.
- Pure logic modules (dispatch/panels/context/hanzo/identity/config) with
vitest coverage; tsc clean; tsup build (library + runnable server).
api.hanzo.ai /v1 via @hanzo/ai only; secrets are env-only (never committed).
@hanzo/meetings — an in-meeting assistant for Zoom and Google Meet over one
shared panel, wired to api.hanzo.ai/v1 via @hanzo/ai and hanzo.id via @hanzo/iam.
- Shared panel (src/panel): model picker + quick-action chips (Summarize /
Action items / Follow-up email) + streaming output + prompt, host-agnostic via
a PanelHost seam. Same UX language as the Office task pane / PDF workspace.
- Zoom App (src/zoom): in-meeting side panel over @zoom/appssdk + a server for
OAuth install and the recording.completed / meeting.ended webhook — verifies
the Zoom HMAC signature, answers endpoint.url_validation, downloads the cloud
recording VTT, and summarizes it via @hanzo/ai.
- Google Meet add-on (src/meet): side-panel web app over @googleworkspace/meet-addons.
- src/hanzo.ts: pure transcript windowing/truncation (honest context note),
WebVTT parse, OpenAI-compatible /v1 request shaping + SSE streaming, summary-
prompt assembly. src/config.ts: endpoints, pickBearer, server env (fail-fast).
- 70 vitest tests (windowing, VTT, request shaping over mock fetch, webhook
signature verify + routing, OAuth shaping, config). tsc --noEmit clean; build
produces dist/zoom, dist/meet, dist/server.js.
- Registers packages/meetings in pnpm-workspace.yaml (one line).
The shared card-builder — one canonical PanelSpec → Slack Block Kit / Teams
Adaptive Cards / Google CardService with stable action ids. The missing
foundation piece for card-host adapters (Slack/Teams/Google); pairs with the
existing @hanzo/ai (headless client) + @hanzo/iam (identity) SDKs. Pure, zero
runtime deps, 66 tests.
One canonical PanelSpec (header, model picker, quick-action chips, output,
prompt input, footer, signed-out state) with stable action ids, emitted to
three host card formats:
- toSlackBlocks → Slack Block Kit blocks[]
- toAdaptiveCard → Teams Adaptive Card v1.5
- toCardServiceJson → Google Workspace CardService JSON
Pure presentation layer: no network, no host SDK deps, no secrets. Emitters
never call the AI or auth (@hanzo/ai / @hanzo/iam do that). assistantPanel()
builder so adapters do not hand-assemble. 66 vitest tests, tsc clean, tsup
build (cjs+esm+dts). Registered in pnpm-workspace.yaml.
Clio practice-management integration — OAuth2 + Clio API v4, AI actions over
matters/documents with write-back to matter notes/activities, and Custom Actions
to embed AI buttons inside Clio. A hosted web app + token-exchange service
(clio.hanzo.ai; client_secret server-only, KMS), so CI-tested — no sideload
binary. Completes the daily-lawyer surface set (iManage/NetDocuments deferred).
Post-review corrections verified against Clio's own developer docs and the
official clio/example-third-party-application, fixing three ways the first
cut would have failed against real Clio:
1. OAuth: removed access_type=offline (a Google-ism Clio ignores — it would
have yielded NO refresh_token). Clio returns a refresh_token on the
authorization_code grant by default; added the real redirect_on_decline
option instead. Wired the refresh path into the SPA (ensureClioToken →
/oauth/refresh) so a long matter session survives token expiry — the
refresh apparatus is no longer dead code.
2. Custom Action nonce: the nonce is NOT a locally-verifiable HMAC. It is a
single-use, 60s code that the app must REPLAY to the Clio API, which
validates server-side (403 on bad/stale). Removed the wrong local HMAC
verify (and node:crypto from the service); /custom-action now parses the
deep-link and redirects into the SPA carrying matter_id + the nonce, and
getMatter replays it (custom_action_nonce query param) on the first
authenticated call. Corrected the deep-link params to Clio's actual set
(custom_action_id/user_id/subject_url/custom_action_nonce; subject_url is
URL-encoded) — subject_id was never sent.
3. Documents: gate downloads on content_type (isTextExtractable) — PDF/DOCX
(the bulk of legal docs) would have been fed to the model as mojibake,
wasting the context budget. Binary docs now contribute name + type only.
The MIME check matches the subtype so DOCX (whose vendor type contains the
substring "xml") is correctly binary.
tsc clean, 89 vitest tests pass, esbuild build succeeds; re-verified the
browser bundle has zero secret/crypto refs.
New @hanzo/clio package: brings Hanzo AI into a lawyer's Clio matters.
Pick a matter and Hanzo can summarize it, draft correspondence, extract
key dates & obligations, or answer freeform questions over the matter's
documents — then write the result back to Clio as a Note or Activity.
Architecture (one and only one way, secret-safe):
- Clio OAuth2 authorization-code grant. client_secret lives ONLY in the
token-exchange service (server.ts, read from env); the browser SPA never
sees it. Verified: dist/app.js has zero secret / crypto / process.env refs.
- Clio REST API v4 client (clio-api.ts): pure request-shaping wrappers for
listMatters/getMatter/listDocuments/downloadDocument/listContacts and the
write-back createNote/createActivity — URL, headers, fields selection,
pagination all unit-testable; one thin clioFetch/clioDownload at the edge.
- Hanzo chat (hanzo-chat.ts) mirrors @hanzo/office-addin's wire contract:
api.hanzo.ai/v1/chat/completions + /v1/models, no /api/ prefix.
- Four actions + matter-context assembly/truncation (actions.ts, 48k total /
12k per-doc caps, truncation marked).
- Custom Action landing (custom-action.ts + server.ts): parses Clio's signed
deep-link, HMAC-verifies the nonce (constant-time) with the client_secret,
redirects into the SPA on the right matter.
- Configurable regional Clio base (us/eu/au/ca).
esbuild build (SPA iife + Node ESM service), tsc --noEmit clean, 86 vitest
tests pass across config/oauth/api/chat/actions/custom-action/server.
Registered packages/clio in pnpm-workspace.yaml.
The Hanzo AI PDF workspace (PDF.js viewer + AI side panel — summarize, extract
parties/dates/obligations, find risky clauses over filings/contracts). A hosted
static surface (pdf.hanzo.ai over hanzoai/static), so CI-tested like the Google
Workspace add-on — no sideload binary.
Self-contained web PDF workspace — PDF.js viewer + Hanzo AI side panel —
for the surface lawyers spend the most time in (filings, discovery,
executed contracts). Cross-platform (any browser), no Adobe license.
- PDF.js (pdfjs-dist 4.10.38) renders pages to canvas and extracts per-page
text; worker bundled and shipped beside app.js.
- Right panel mirrors @hanzo/office-addin: model picker, quick-action chips,
streamed replies over api.hanzo.ai/v1/chat/completions (SSE), /v1/models.
- Law-office quick actions: summarize; extract parties/dates/obligations;
find & explain risky clauses; explain the selection; draft a summary memo.
- Honest large-PDF context windowing (buildContext): caps attached text at a
char budget, windows whole-document or from the current page, and prepends a
context note naming the exact page range sent — never claims the full doc
when truncated. Pure and unit-tested.
- Auth: pasted hk- key validated against /v1/models before save (localStorage);
OAuth via hanzo.id documented as the additive future path.
- esbuild build.js, vitest (44 tests), tsc clean.
Endpoints: api.hanzo.ai /v1 only.
The Safari step zipped a fixed 'safari/Hanzo AI' subpath and hit zip exit 12
('nothing to do') → continue-on-error swallowed it → no release asset (v1.9.30
shipped without Safari). Zip the whole dist/safari tree (the Xcode project is
always emitted; a dev builds+signs+sideloads it) + any built .app, guarded so a
zip exit code can't blank the asset. Future releases carry Safari reliably.
Adds the Outlook mail add-in as a downloadable release binary (hanzo-ai-outlook)
alongside Office, browser, IDE (vscode/cursor/windsurf/antigravity/jetbrains),
Claude, and Safari. Google Workspace ships via clasp, not a sideload binary.
Wire the two new productivity surfaces into CI/CD:
- ci.yml: test @hanzo/outlook-addin (39) + @hanzo/gworkspace-addon (29).
- publish.yml: new 'outlook' job builds @hanzo/outlook-addin → hanzo-ai-outlook-v<ver>.zip,
added to release needs/files + an Outlook row in the download table.
(Google Workspace deploys via clasp to Google, not a sideload binary — CI-tested
only, no release zip.)
- pnpm-lock: workspace now includes packages/outlook + packages/gworkspace.
buildPlugin transitively runs buildSearchableOptions, which launches a headless
IDE (JCEF) needing native font libs (libfreetype.so.6) absent on the CI runners
→ UnsatisfiedLinkError, non-deterministic (only green when the task was cache
UP-TO-DATE, which is why ci.yml passed but the release build failed). The search
index is optional and rebuilt by the IDE at runtime; disabling it makes
buildPlugin reliably emit the distributable plugin zip for the GitHub Release.
Bumps root + browser + vscode + office + jetbrains to 1.9.29 so every release
asset name matches the tag (the download-table URLs use the tag version). The
first release to carry a downloadable JetBrains plugin binary alongside browser,
VS Code/Cursor/Windsurf/Antigravity, Claude, Safari and Office.
Five surfaces (browser, office, vscode, cli, mcp) each hand-rolled the SAME
IAM OAuth2+PKCE flow — and one drifted and broke: vscode built
hanzo.id/oauth/authorize and iam.hanzo.ai/oauth/token, both MISSING the
/v1/iam prefix (a 404 — the same class of bug as the browser login fix),
plus it POSTed JSON where IAM requires form-urlencoded and sent a
client_secret on a public PKCE client. That is the opposite of one way.
Decomplected into ONE core — packages/auth (@hanzo/auth):
- config.ts canonical HIP-0111 endpoints (authorize/token/userinfo ALL
carry /v1/iam) + the one-client-per-surface registry
(hanzo-browser/office/vscode/cli). URL assembly lives once.
- pkce.ts the one PKCE (S256), platform-CSPRNG + WebCrypto.
- oauth.ts buildAuthorizeURL / exchangeCode / refreshTokens / userinfo —
pure fetch, form-encoded per RFC 6749.
- flow.ts login / getValidToken (transparent refresh) / logout /
currentUser, written ONCE against TokenStore + Opener. A
surface supplies only those two thin adapters — no surface
re-implements the flow.
23 tests: the drift guard (every path has /v1/iam), client registry, PKCE
RFC-7636 vector, wire shapes, and the full flow with fake adapters.
Migrate office onto it: roamingSettings TokenStore + Dialog-API Opener;
delete office's duplicate pkce.ts + IAM config (now the core's). API-key
path kept. 22 tests; the core bundles into the task pane.
Fix vscode standalone-auth: all three endpoints → ${host}/v1/iam/oauth/*,
token calls form-urlencoded, OIDC scopes, drop the secret on the public
PKCE client. Compiles clean.
CI now gates @hanzo/auth. Browser/mcp/tools already use canonical paths;
migrating them to import the core is the tracked next step (each needs its
own bundler wiring) — the ONE way now exists and has two consumers.
The GitHub Release table advertised JetBrains and Safari downloads that 404'd —
neither job produced a release artifact — and the Office add-in was absent.
Make every advertised binary real and deterministic from the tag:
- jetbrains: always `gradlew buildPlugin` → hanzo-ai-jetbrains-v<ver>.zip artifact
(Marketplace publish stays token-gated); the job no longer skips without the
token, so the download link always resolves.
- office: new job builds @hanzo/office-addin → hanzo-ai-office-v<ver>.zip.
- safari: package the built .app(s) → hanzo-ai-safari-v<ver>.zip.
- release: add jetbrains+office to `needs`, add all three globs to `files`, and
add the Office row to the download table. Asset names use the tag version so
they match the table URLs.
All-platform release: browser (chrome/edge/firefox/safari), IDE (vscode/cursor/
windsurf/antigravity/jetbrains), Claude dxt, and Office (Word/Excel/PowerPoint).
Additive enhancements to the Office add-in, keeping the existing ask/requestBody
contract (and its tests) intact:
- chat.ts: askStream (SSE streaming completion → onDelta), streamDelta (pure SSE
frame parse), listModels (/v1/models → ids); requestBody gains an opt-in stream
flag (defaults false, so the one-shot ask path is unchanged).
- taskpane: a live model <select> populated from /v1/models (org-scoped by the
token, refreshed on sign-in), and one-click quick-action chips — Draft,
Summarize, Explain plainly, Tighten/redline, Continue — that run over the
selection. Output now streams in live; Insert stays gated on real content.
Built for document-heavy review/drafting (e.g. a law office). Tests: 31 pass
(+6 new: stream flag, streamDelta, listModels). Build OK; my files tsc-clean.
- API-key auth: paste a Hanzo hk- key (console.hanzo.ai → API keys) instead
of the IAM OAuth dialog, so the add-in is usable with zero infra/IAM setup.
pickBearer(apiKey, oauthToken) precedence is pure + tested (29 tests).
- Windows test kit: `HANZO_OFFICE_BASE=https://localhost:3000` build ships
serve.mjs (stdlib HTTPS static server) + SIDELOAD-WINDOWS.md, so the
downloaded zip runs on any machine — trust a localhost cert, node serve.mjs,
sideload manifest.xml, paste key, go.
- CI Build now emits BOTH hanzo-ai-office-v${VER}.zip (production, office.hanzo.ai)
and hanzo-ai-office-localhost-v${VER}.zip (the test kit).
Bump browser+root to 1.9.28 so tagging v1.9.28 publishes a GitHub Release
carrying ALL binaries — chrome/edge/firefox/safari, vscode/cursor/windsurf,
claude(dxt), and now office (prod + localhost) — with permanent download links.
The surface a non-developer actually uses — a Microsoft Office task-pane
add-in that puts Hanzo AI inside the document. Select text or a range,
ask, insert. Distinct from the browser extension and the IDE extensions;
same backend as everything else: model calls through api.hanzo.ai/v1
(OpenAI-compatible), sign-in via Hanzo IAM (hanzo.id, auth-code + PKCE,
HIP-0111). No new API surface.
Decomplected so the logic is pure and unit-tested (26 tests), Office.js
glue kept thin:
- chat.ts — request/response shaping (buildMessages fences the selection
as data; extractContent tolerates the gateway's response shapes)
- pkce.ts — PKCE + authorize URL, verified against the RFC 7636 S256 test
vector
- host.ts — per-host read/insert; Excel range↔text helpers tested
- config.ts — canonical endpoints pinned (api.hanzo.ai, /v1/iam/oauth/*)
- taskpane/auth/commands — the Office.js + Dialog-API glue
manifest.xml passes `office-addin-manifest validate` ("The manifest is
valid" — Word/Excel/PowerPoint on web, Windows, Mac). build.js stamps the
hosting base URL (office.hanzo.ai, override for localhost dev) into a
sideloadable/AppSource-submittable dist/manifest.xml.
CI runs the tests and builds+zips a sideloadable artifact. Publishing to
AppSource needs the dist hosted at office.hanzo.ai + a hanzo-office IAM
client + a Partner Center submission (documented in the README).
The package's `npm test` has been broken-by-construction for years:
tsconfig excluded src/test entirely (out/test/runTest.js never existed),
tsconfig.test.json inherited the exclude that negated its include, the
runner pointed at a ./suite/index path that never existed, two suites used
BDD describe/it under the tdd-configured mocha, a vitest file sat in the
mocha glob, `ws` was never declared (it resolved only cross-package), and
154 eslint errors blocked pretest.
Lint 154 → 0 with real fixes: the ban-types autofix damage reverted
properly (the local `Symbol` interface shadowing the ES built-in — the
actual footgun — is now `CodeSymbol` everywhere), case bodies braced via
AST codemod, empty catches annotated, `Function` types replaced with real
signatures, hasOwnProperty via Object.prototype, and
@typescript-eslint/no-var-requires off — CJS lazy requires are the
intentional activation-perf idiom (the rule is retired to stylistic in
typescript-eslint v8).
Harness: tests compile via tsconfig.test.json in pretest; runner path
fixed; BDD→TDD; glob v10 promise API; esModuleInterop default imports.
Two harnesses, one directory each: vitest owns src/test/vitest/ (scoped
vitest.config, excluded from the mocha compile), mocha owns the rest;
posttest runs the vitest side so `npm test` is the whole gate.
Suites modernized against current tool contracts (34 passing / 64 failing
→ 152 passing / 0 failing / 12 pending): web-fetch rewritten against the
real http(s) path via loopback server, mode against list/activate/show/
current, mcp-tools against config-driven getAllTools filtering, extension
pins the shipped manifest contract, shell/process/git-search/filesystem/
config/bash/mcp-runner exercise real commands, files, repos and spawns.
Dead-subject suites removed; network-dependent integration tests gated on
HANZO_E2E_LIVE with visible skip reasons (12 pending). Tests-only — the
one suspected product bug was an unfaithful Memento mock, and a real
cross-suite settings leak was fixed by making suites hermetic.
CI: cli-tools and vscode test steps lose continue-on-error and become
hard gates (vscode under xvfb-run, same as the E2E job).
The first pass pinned each override to its advisory-snapshot floor, but
several packages carry stacked advisories — a pin at one advisory's floor
sits inside the next one's vulnerable range (protobufjs 7.5.5 vs 7.6.3,
tar 7.5.16, tmp 0.2.7, flatted 3.4.2, vite 6.4.3, file-type 21.3.2,
qs 6.15.2, js-yaml 4.2.0). One within-major floor per package now, at the
highest first-patched. Suites: browser 243, ai 31, aci 29, cli-tools 87.
- pnpm overrides pin every vulnerable transitive to its advisory's exact
first-patched version, scoped to the advisory's own vulnerable range so
no dependency is dragged across a major it didn't need (criticals:
form-data 4.0.6, handlebars 4.7.9, protobufjs 7.5.5).
- vitest < 3.2.6 is a critical (CVE fix floor) with no 0.x/1.x backport —
forced major to ^3.2.6 in every package. Test scripts unify on
`vitest run` (aci/ai ran bare watch mode). tools' vscode mock alias
becomes absolute (vitest 3 dropped relative alias resolution).
- packages/ai otel family 1.x → 2.x (core <2.8.0 advisory covers all 1.x):
resourceFromAttributes, ATTR_* semconv, constructor spanProcessors. The
previously-uncollectable telemetry suite now runs: ai 31 tests (was 16).
- direct bumps: vite ^6.4.3 (site), esbuild ^0.25.8 (mcp), uuid ^11.1.1
(tools). packages/ai/package-lock.json deleted — stray npm lockfile in a
pnpm workspace, carried 24 of the alerts.
- ci: test step filtered @hanzo/tools but the package is @hanzo/cli-tools —
the suite never ran in CI. Fixed; 87 tests now gate (behind the existing
continue-on-error). pnpm 9 reads onlyBuiltDependencies from package.json,
not pnpm-workspace.yaml — mirrored so native build scripts stay allowlisted.
Verified: browser 243/243 + e2e 30/30 + build; aci 29/29; ai 31/31;
cli-tools 87/87; site builds on vite 6; mcp builds on esbuild 0.25.
1.9.25 shipped the unregistered app-hanzo client_id, so the PKCE token
exchange failed invalid_client — login has been dead since v1.8.0. The
canonical registered client is hanzo-browser (universe IAM init_data;
redirectUris include https://hanzo.ai/callback). Contracts pinning the
registered client land in shared-config + cross-browser-parity tests.
Adopted from fix/dxt-login-and-ci-guards: the DXT auth tool offered a
password-grant login defaulting to the unregistered app-hanzo client —
removed; whoami/token/account read /v1/iam/oauth/userinfo, refresh
exchanges at /v1/iam/oauth/token, issuance is authorization-code+PKCE
only. dxt build script now actually builds (mcp cli → server.js) instead
of echoing success. PR #6's test-suite restorations are not taken — main
already restored those suites.
Migrate the last llm.hanzo.ai references (vscode MCP llm tool provider URL
+ gateway fallback, auth-chat e2e doc comments) to the canonical
https://api.hanzo.ai/v1 (OpenAI-compatible, IAM JWT at the gateway — see
hanzoai/ai). The browser extension chat paths already pointed at
API_BASE_URL = api.hanzo.ai.
model-hub's HuggingFace tests are real live-API integration tests, which
means they fail on HF's weather: repeated runs rate-limit and the 5s
default timeout reads as 6 test failures with nothing of ours broken.
Probe HF once at module load and describe.skipIf with the status logged
when HF itself refuses service; give live calls a 15s budget. A failure
while HF answers 200 is still a real failure. 243/243 across repeated runs.
Three specs still tested the pre-1.9.3 popup (#open-page-overlay FAB,
'Open Chat Panel' label) — the Extension E2E jobs have been red on them
since the FAB was killed. Repointed at the current contract
(#overlay-enabled + #open-panel, 'Open Chat').
The interaction specs (overlay show, picker tool, console tool) all
funnel through the popup's active-tab lookup, which cannot work when the
harness hosts the popup as a tab: the popup IS the active tab, so the
privileged-tab guard always takes the fallback. Each spec now sends the
exact message its button sends (page.overlay.show / tool.picker.start /
tool.console.start), explicitly targeted at the example.com tab, so the
background relay and content-script surfaces are still exercised end to
end. CI's file set (popup, popup-tools, sidebar): 30/30 locally.