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

@slopus/happy-agent-client

v0.0.92

Published

Typed client for the Happy agent HTTP API.

Readme

@slopus/happy-agent-client

A typed client for the Happy agent HTTP API, specified endpoint by endpoint in packages/happy-agent/API.md.

Surfaces are named instances of predefined shared features. createSurface() creates a todo list with its own CUID2 ID, name, and optional managed image. listSurfaces() returns the catalog with members nested inside every Surface, [] when nobody joined. Each member's orderKey places that Surface in that person's sidebar. joinSurface(), leaveSurface(), and reorderSurface() change only the caller's member record. Standalone members have userId: null, as in the profile; team members use their installation-local Happy user ID. Desktop bootstrap's optional surfaces field composes these same objects. Older daemons return 404 or 501 for the catalog. Follow shared surface.updated and use its normal version chain to reconcile the complete member array.

The first Surface type is Todo, served at /v0/surfaces/todo/:surfaceId. Every todo method takes the Surface ID first. listTodos(surfaceId) loads every active todo in fractional key order; use scope: "completed", "deleted", or "all" for other states. createTodo() appends; supply an ID to resolve an uncertain creation result. updateTodo() edits text or status, and reorderTodo() moves active items only. deleteTodo() retains a tombstone; updating an inactive todo to status: "active" appends it at the end. Every mutation of an existing todo takes ifMatch with its current version. listTodoVersions() and getTodoVersion() expose permanent full revisions with trusted actor and source details. To apply a previous state, read its fields and use ordinary update and reorder operations with the current version. There is no separate restoring command. Todo access never joins the caller. Follow shared todo.created and todo.updated from the list cursor, refetching on a version gap. The client exposes these events without adding an automatic Todo store to HappyReducer.

HappyAgentClient is built from an endpoint and a bearer token. The token may be a function called for every request, so refreshed credentials apply without a new client; it may be omitted only to call getAuthentication() before signing in.

getAuthentication() reports whether the client is signed in and lists sign-in methods. An oauth method uses the authorization code flow with PKCE, talking to the deployer's authorization server directly; the daemon never sees the code, the PKCE verifier, or a refresh token. beginOAuthSignIn(method, { redirectUri }) returns the URL to open in the system browser, the server's host to show the person, and the state and verifier to keep in memory. completeOAuthSignIn(signIn, callbackUrl) checks state and exchanges the code at that method's token URL. refreshOAuthCredential(credential) refreshes only at the refresh URL recorded at sign-in, never at one a daemon advertises later. Failures throw HappyAgentOAuthError with an OAuth or client code. In JWT team mode, a 401 HappyAgentApiError carries the same methods in authentication. Unknown method types are skipped. Older daemons return 404 for discovery.

Remote connection rosters are read with listConnections(). The typed connections.updated event carries the complete { connections, version } snapshot: keep the greater UUIDv7 version across list responses and events, ignoring duplicate or older snapshots. An empty array clears the roster. Capture a cursor before the initial roster read and follow updates after it to avoid missing a concurrent change. Refetch on state loss or daemon replacement. The list's version is optional for older daemons. authentication: "workos" identifies team remotes and includes organizationId; "bearer" identifies standalone remotes. Older daemons may support roster reads without emitting this additive event. connection(id) creates a separate client for the selected remote, without merging its events or state into the parent.

Ordering-capable daemons return a required fractional orderKey on every connection, backfill existing connections in their previous order, and append new ones at the end. The roster is already sorted by key. Use reorderConnection(id, { afterId, mutationId }, { ifMatch: version }) to move one after another, or pass afterId: null to move it first. The response and the existing connections.updated event carry the complete ordered roster; neighbours retain their keys. Older daemons may omit keys and return 404 for reordering; leave the feature unavailable there.

Runners are read with listRunners(). The typed runners.updated event carries the complete { runners, version } list, in ascending ID order, after any runner changes, connection status included. Keep the greater UUIDv7 version across list responses and events, ignoring duplicate or older lists. Capture a cursor before the initial read and follow updates after it to avoid missing a concurrent change; refetch on state loss or daemon replacement. Each runner reports its status, whether it is the default, the machine it last reported (or null before it has ever connected), its runner protocol, since, and the last disconnection reason. Tokens and transport addresses never appear. Runners require protocol 26: older daemons answer 404, so treat runners as unavailable there.

A folder on a runner has a { type: "runner", runnerId, path } compute, with path in the runner's filesystem and null for the home project before its runner first connects. Docker workspace computes may carry the mounted path and the holding runnerId. registerProject() and cloneProject() accept an optional runnerId; a runner project's defaultWorkspaceCompute is { type: "runner", runnerId } or { type: "docker", image, runnerId }. Work on a folder whose runner is not connected fails with 503 runner_unavailable; registering or using a folder on the daemon's own machine while runners are configured fails with 409 local_execution_disabled. A compute type this client does not recognize means the folder is on another machine with no local-path actions.

Installation display information is config.node, read through getConfig() and already included in desktop bootstrap's config. Its avatar is either { thumbhash } or null when no image is set. Use patchConfig({ node: { name } }) to rename the installation independently of p2p.name; avatar mutations belong to admin-bot tools. getNodeAvatar() fetches authenticated image bytes, accepts ifNoneMatch, and returns null for 304. An absent image rejects with 404. The existing config.updated invalidation covers name and avatar changes: refetch config and conditionally refetch image bytes even if the ThumbHash is unchanged. Serialize config refreshes and refresh again if an invalidation arrives during a read. Follow the bootstrap cursor and refetch on state loss or daemon replacement. Older compatible daemons may omit config.node and return 404 for the avatar route; absence means this feature is unavailable, not that the node has no image.

HappyReducer is the stateful layer over that feed. Construct it with a client, register update listeners, and start it when the application wants live synchronization. getState() and subscribe() expose a read-only Zustand-style external store suitable for useSyncExternalStore: the snapshot reference changes only when state changes, and unchanged agent children retain their references. Every listener registered with subscribeUpdates() receives every original ordered SSE item—connection changes, state loss, and ordinary events—after reduction, together with the current snapshot. Connection state includes draining while the daemon remains connected for reads but no longer admits mutations.

State contains connection and an agents record keyed by Agent ID. Calling agentVisible(id) registers visible interest and returns an idempotent cleanup that lowers the agent to background priority. One agent bootstrap supplies its draft, last-used provider/model, context occupancy, pending input, current activity phase, processes, and direct subagents. The reducer also reads the focused question endpoint, and calls the separate activity endpoint only when an older compatible daemon omits the additive activity fields. Pending messages leave state when a run accepts them; the question becomes null when it is answered or canceled. At most three agents sync at once; visible agents are selected before tracked background agents. The reducer opens SSE first, retains a bounded 60-second event window, and reconciles each field against its private cursor before reapplying events received during snapshot loading. A stream gap or broken resource-version chain marks affected agents dirty and queues an authoritative refresh. Failed reads retry with exponential backoff.

Stopping is synchronous: it immediately makes the reducer disconnected, aborts snapshot reads, and ignores late results. A later start resumes the SSE cursor and refreshes every tracked agent.

Happy integration state is available in the desktop bootstrap and through focused read, start, cancel, disconnect, and re-pair methods. Its status is a discriminated union: pairing always has renderable opaque QR data, failure always has a display-safe error, and connected states always carry configured credentials. A desktop client installs the bootstrap snapshot, follows complete happy.integration.updated replacements from the bootstrap cursor, and keeps the greater version. In team mode, these same methods, bootstrap, and events transparently address the authenticated user's personal mobile connection. Ownership is resolved inside the daemon, not exposed as a new snapshot field or request argument. Routes, wire shapes, and the protocol version remain unchanged; no SDK update or capability negotiation is needed. Discard the old snapshot and restart bootstrap and event subscriptions when switching authenticated users. The daemon filters private mobile events in both pulls and SSE; a pull page may contain no visible events while advancing its cursor past another user's events. The integration remains separate from required onboarding, so a product may present pairing as an optional onboarding screen or later in settings without changing onboarding completion.

getUsers() resolves the authors in message metadata.userId. Team mode resolves members and guests; standalone mode, where getProfile() describes the one person, resolves only guests. A guest is someone who reaches an exported bot from an outside chat service: kind is "guest" and canLogIn is false, and a guest has no photo. Older daemons omit both fields, so treat a missing kind as "member" and a missing canLogIn as true; older daemons also answer 404 in standalone mode.

User settings are the caller's own opaque key-value preferences, kept on the daemon so they follow the person across devices: the authenticated member's in team mode, the installation's one person's in standalone mode. The daemon checks only shape and size. Prefix keys with your application's name and a dot, such as happy-desktop.completionSound. Read userSettings from desktop bootstrap, or use getUserSettings(), and follow user_settings.updated from that response's cursor. A snapshot replaces the whole local copy. For each event, keep the entry with the greater version for its key. saveUserSetting(key, { value, updatedAt, mutationId }) sets a value, and value: null clears it. Each key is last-write-wins by updatedAt. Send an offline change with the time the person made it, and adopt the returned entry: it is unchanged when the write was stale. A clear keeps its version, so a stale device cannot bring the value back. getUserSetting(key) returns an unset entry, never 404. Invalid keys throw before any request, and the exported MAX_USER_SETTING_* constants are the specified limits. Discard the copy when the authenticated user changes. An absent bootstrap field, or 404 from getUserSettings(), means the daemon has no user settings, so keep those preferences on the device. Through connection(id), settings belong to the remote daemon and the person it authenticates.

The global secrets surface exposes only safe metadata: descriptions, environment-variable names, availability, attachments, versions, and timestamps. Raw values appear only in typed create and update request bodies. Project, workspace, and exact-agent attachment targets are discriminated by type; attach results preserve the meaningful 200 versus 201 status. The event union includes versioned secret changes and immutable attachment creation/removal without introducing a value- bearing response or event type.

const reducer = new HappyReducer(client);
const hideAgent = reducer.agentVisible(agentId);
const removeUpdateSubscription = reducer.subscribeUpdates((update, state) => {
    console.log(update.kind, state.connection);
});
const removeStateSubscription = reducer.subscribe((state, previousState) => {
    console.log(previousState.connection, "→", state.connection);
});

reducer.start();
console.log(reducer.getState());
reducer.stop();
hideAgent();
removeUpdateSubscription();
removeStateSubscription();

It is built on plain Web APIs — fetch, streams, AbortController, standard timers — so the same build runs unchanged in Node and in a browser. The daemon listens on a Unix domain socket; a caller reaching one supplies its own runtime's socket-capable fetch, and the client never dials a socket itself or reads credentials from disk.

applyMessageDelta implements the protocol's offset-aware text reduction without adding client state: exact and overlapping replays converge idempotently, while a gap or conflicting overlap returns reconcile so the caller can replace the message from authoritative history.

Protocol shapes live in sources/protocol/, one file per API chapter, with shared wire values declared as TypeBox schemas and their TypeScript types derived with Static.

Live voice sessions

createLiveSession({ id, windowId, sdp, credential, contextRevision, context }) binds GPT-Live to the initiating desktop window and returns its resource and WebRTC SDP answer. Select the server-held credential explicitly as { providerId, type: "codex_subscription" } or { providerId, type: "openai_api_key" }. The former is experimental and does not promise subscription entitlement; the latter opts into API billing. There is no automatic fallback. The client never receives a provider token or API key.

Client microphone/speaker <— WebRTC —> GPT-Live
Client — authenticated SDP —> Happy daemon — sideband —> GPT-Live
                                   └— typed UI actions/context <—> initiating window

Use a stable caller-chosen id: creation is never automatically retried, and a repeated ID returns a conflict with the current session rather than starting another billable call. After an uncertain outcome, use getLiveSession(id) and close before deliberately replacing it. Follow owner-private live.session.created and version-chained live.session.updated events through updates(). Refetch on gaps; HappyReducer does not maintain a second voice store.

closeLiveSession(id) may return closing. closed means a deliberate end completed, not that usage was finalized; only usage.final confirms the final provider duration. A failed connection may have usage.seconds: null, which is unknown, not zero. Voice closure never aborts coding sessions or their tasks. The desktop explicitly selects at most five watched sessions. These additions do not change existing protocol compatibility. Older daemons may return 404 or 501; leave voice unavailable rather than changing authentication or silently enabling billing.

Require getHealth().capabilities?.desktopLiveControl === true before offering voice startup. liveSessionControlUrl(id, windowId) builds the authenticated WebSocket upgrade address; the trusted desktop host supplies bearer headers, never URL credentials. The exported LiveControlClientMessage and LiveControlServerMessage schemas define both directions. After verifying hello identifies the expected call/window, wait for status: active.

The fixed LiveDesktopAction union contains state/open, workspace/session/bot creation, public session read/watch, exact-text message staging, and non-overwriting draft append. The desktop validates target namespaces, context revisions, permissions, and action identities. It never exposes generic shell/API/JavaScript execution or permission answers. Transcripts are provider-derived fragments, not authoritative user confirmations. Both interval fields are present only when the provider supplies real timing; native untimed fragments omit both. A sessionSend result with output { type: "staged" } means the text awaits an independent human Send, never that it was submitted. Context contains only bounded visible names, IDs, public user/assistant text, structured status, and draft presence. A control disconnect ends voice; already-running coding work continues. There is no automatic controller reconnect or mutation replay, and no older Realtime fallback.

Sandboxed workspace services

Services are started by an agent's separate service_start tool. This client manages and observes the same execution; it does not expose another command-launch route or a global services catalog.

Workspace client → list/get service → shared process input/output
                                  → stop and observe confirmed teardown
                                  → scoped credential + fixed-endpoint CONNECT

Use listWorkspaceServices(workspaceId) and getWorkspaceService(workspaceId, serviceId) for discovery. Follow the list's journal cursor through updates() and reconcile service.created and version-chained service.updated events. Refetch on gaps or daemon replacement. Services are an on-demand surface; HappyReducer does not materialize a second service store automatically. For a known workspace, 404 or 501 means the feature is unavailable, not an empty catalog.

inputWorkspaceService(workspaceId, serviceId, { readerId, chars, waitMs }) reads when chars is empty and otherwise writes stdin then reads. Give every UI reader its own stable readerId so views do not consume each other's output. This ID grants no permission. Neither reads nor writes are retried automatically: a lost write response may already have delivered input. The response includes bounded output, an explicit truncation flag, and the current service snapshot.

stopWorkspaceService() records a stop decision. A response with status: "stopping" does not prove the process tree is gone. Workspace serviceCleanup progress reports the mandatory barrier before deleting files; a blocked cleanup keeps the files intact.

Trusted desktop hosts use issueWorkspaceServiceAccessToken() and workspaceServiceProxyUrl(). The latter returns an address only: supply normal API authorization and the separately scoped WORKSPACE_SERVICE_AUTHORIZATION_HEADER on CONNECT. Keep these credentials out of URLs, page JavaScript, ordinary application requests, and unrelated origins. The same methods work through client.connection(id), preserving that connection's authenticated route. No public sharing or invitation is created by issuing a token.

Local bridges

A bridge brings a USB device, a USB Bluetooth adapter, or a local TCP service from the client's machine to a workspace's machine. The client opens it with a WebSocket and holds it; closing that WebSocket ends the bridge.

Desktop USB/IP server ⇄ bridges/usb WebSocket ⇄ daemon USB/IP client ⇄ vhci_hcd on the workspace's machine
Program on the workspace's machine → 127.0.0.1:port → bridges/port control: { type: "connection" }
                                                    → bridges/:bridgeId/connections/:id ⇄ local target

Require getHealth().capabilities?.workspaceBridges === true, then read listWorkspaceBridges(workspaceId) for the workspace's capabilities and open bridges. Each capability names a reason when unavailable; show its message. Follow the list's cursor through updates() for bridge.created, version-chained bridge.updated, and the bridges.capabilities.updated invalidation. 404 means the daemon has no bridges.

workspaceUsbBridgeUrl(), workspacePortBridgeUrl(), and workspaceBridgeConnectionUrl() build the WebSocket addresses; supply the bearer token on the upgrade. WorkspacePortBridgeServerMessage and WorkspacePortBridgeClientMessage define the port bridge's JSON control messages, and WORKSPACE_BRIDGE_CLOSE_CODES names the close codes. closeWorkspaceBridge() closes one bridge. Forwarding a local port into a workspace uses workspaceProxyUrl() with a nested CONNECT. All of these work through client.connection(id).

HAPPY_AGENT_PROTOCOL_VERSION is 26; HAPPY_AGENT_MIN_PROTOCOL_VERSION is 22. The range is additive, so clients should not require exact protocol equality for existing features. Creating a bot without name requires a daemon advertising protocol 24 or newer. With an older compatible daemon, supply a deliberate name or leave unnamed creation unavailable. An application that requires unnamed creation may require protocol 24 throughout its UI.

createBot({ id, workspaceId, agentId }) supports optimistic creation on protocol 25+. The caller checks the daemon's protocol and may open the conversation locally, queuing sends until creation succeeds. Keep id for retries; conflicting child IDs return 409. Omitted IDs are daemon-generated.

Tasks are additive without a protocol bump. Detect support through listTasks(): 404 means the daemon has no tasks. listTasks() returns every task, oldest first, and listTasks({ scope: "joined" }) only the caller's joined tasks in the caller's order; both carry the caller's memberships in that order. joinTask(), leaveTask(), and reorderTask() change only the caller's own list. Leaving never archives. archiveTask() and unarchiveTask() take the task's version as ifMatch and succeed only when the task reports canArchive; resolve the owner through getUsers() and getUserPhoto(). createTask() creates a task the caller owns, named or left to name itself from its first message, and renameTask() renames it. A task's workspaceId works with every workspace route, files, terminals, and Git included. Follow task.created and task.updated for the tasks, and task.joined, task.reordered, and task.left for the caller's memberships.

Artifacts are additive without a protocol bump. Detect support through listArtifacts(): 404 means the daemon has no artifacts. The catalog is global and pages newest-created first; filter it by type, sourceKind/sourceId, agentId, or authorKind/authorId, and follow nextPageCursor. Every artifact names its extensible source and its createdBy, updatedBy, and deletedBy authors. Each version is a small tree of files: getArtifactVersion(artifactId, number or "latest") returns its manifest and its entry, the file to open. Read a file by its path with getArtifactFile(artifactId, version, path), ranges included, or hand artifactFileUrl(artifactId, version, path) to a frame or media element; every file of a version shares one URL prefix, so relative references in a Markdown or HTML entry resolve. People publish binary files with uploadArtifactFile() and place them at paths in createArtifact() or updateArtifact(), next to text files given inline as { path, text }. updateArtifact() makes a new version from the latest one's files, adding, replacing, or removing paths, and deleteArtifact() leaves a tombstone; both take the artifact's version as ifMatch. listArtifactVersions() reads the history. Follow artifact.created, artifact.updated, and artifact.deleted.

MessageMode.account is optional server-resolved provenance only in configuration/error execution snapshots, not an account selector. It is absent from user-message/composer modes and drafts. null means unestablished; a known account has an opaque local ID and optional human-readable label. MessageMode.providerType optionally records the canonical configured provider family for that execution. The client omits both output-only fields from sends, slash-command request modes, and saved drafts without mutating the caller's snapshot, preserving compatibility with older daemons. Configuration service messages carry actual before/after execution modes, where providerId is the concrete route rather than the requested routing alias. Their beforeMessageId anchor is nullable: null retains chronological append placement. Desktop can honor an anchor; an append-only terminal appends when inserting it would move already-stable history. The initial before: null record is a hidden baseline. Render model names from the catalog and account-only changes within the same known provider family, preserving unknown identities honestly. Family comes from the persisted snapshot's providerType; Bedrock remains Bedrock regardless of the model vendor. Never derive historical family from the current catalog or provider/model ID prefixes. A same-family concrete provider change still shows a configured-connection change when vendor account IDs are unknown. Use configured labels or exact route identifiers, without presenting those connection names as verified vendor identity.

Error service messages retain full multiline text and may carry error: MessageError with the actual failed mode, a safe credential-source descriptor, manual steps, recovery actions, and optional copy-only recovery commands. Commands are typed { label, command } values, never extracted from error text or executed automatically. Manual recovery remains available without another AI credential. Source kinds and action names are extensible strings. Use knownCredentialSourceKindSchema and knownErrorRecoveryActionSchema for recognized semantics; ignore unknown actions and do not guess a source-specific remedy for an unknown kind. The current action allowlist is ask_chief. Details supplement text rather than replacing or clipping it. The Chief of Staff action uses the existing bot catalog's systemKey to open a diagnostic draft for review, preserving existing draft text and model selection. Sending requires an explicit user request and ordinary sendMessage(); normal tool permissions and manual instructions remain intact. No credentials or credential-file paths belong in those details. Older messages without this metadata remain valid.

Global skill management is additive without a protocol bump. Detect support through listGlobalSkills(): 404 or 501 means unavailable; protocol 25 alone does not guarantee support. listGlobalSkills() includes disabled and broken skills; getGlobalSkill(id) returns parsed metadata, the original document, and its Markdown body. updateGlobalSkill(id, { enabled, mutationId }, { ifMatch: skill.version }) changes availability without deleting files. listGlobalSkillFiles() pages through supporting files, and readGlobalSkillFile() reads their original bytes, including while disabled.

Follow skills.updated from the first catalog page's event cursor. Its bounded skillIds and root-relative paths invalidate affected catalog pages, details, and open files; null means refresh broadly. An enablement-only change has paths: []. Page cursors are separate from journal cursors, and a 409 during pagination requires restarting the list. Refresh again if an event arrives during a read, keep the newer summary version, and resync on state loss or daemon replacement. The client exposes the feed without automatically caching skills in HappyReducer. Older daemons' missing endpoints mean this feature is unavailable, not that no skills are installed.

Tool calls expose the complete ToolPresentation discriminated union — exploration, command, background-terminal interaction, file diff, web/X search, sub-agent creation, media, and artifact — together with an exported TypeBox schema for each variant and toolPresentationSchema for the whole set.

MediaPresentation lists the images, videos, PDF documents, and other files a completed call showed inline: generated images and files presented with present_files. Image items always carry displayed width, height, and a base64 thumbhash to show until the bytes load; render an unknown item kind as a file. Read an item's bytes with getToolCallMedia(agentId, toolCallId, index), ranges and ifNoneMatch included, or hand toolCallMediaUrl(agentId, toolCallId, index) to an image, video, or document element through a transport that adds the bearer token. The URL's content never changes. ArtifactPresentation carries the artifactId, produced version, artifactType, and title a card needs to open an artifact a call created or updated. Both are additive; older daemons omit them, and their blocks keep raw tool data even with omitToolData: true.

AgentSpawnPresentation carries an optional complete model: { modelId, providerId, name } identity resolved before child creation, and an optional agentId after creation and initial-task delivery succeed. The enclosing tool status owns the spawning/completed/failed lifecycle, not the child's later work. Use the catalog model.name for the inline label, or a generic sub-agent label when absent; never infer model identity from task titles or raw tool data. The daemon retains the resolved identity across history loads and restart. Older daemons may omit this additive presentation; older clients can use raw tool data, which remains present for agent_spawn even with omitToolData: true. No protocol-version bump is required.

Cloud exposes WorkOS authentication and organization management. Use getCloud() for local status, startCloudAuthorization() and completeCloudAuthorization() for PKCE sign-in, mintCloudAccessToken() for a verified access token, and disconnectCloud() for local sign-out. listCloudOrganizations(), createCloudOrganization(), and deleteCloudOrganization() manage the connected user's organizations. Desktop bootstrap includes the Cloud snapshot; follow cloud.updated replacements and keep the greater version.

Focused agent responses and agent bootstrap include the current module-contributed slash-command catalog. invokeSlashCommand executes one through its owning module, while agent.slash_commands.updated carries complete catalog replacements discovered at turn time.

Reference tags

Agent text may mention Happy entities as <happy-artifact id="…"/>, <happy-agent id="…"/>, <happy-workspace id="…"/>, <happy-project id="…"/>, or <happy-service id="…"/>, as API.md specifies under "Reference tags". This package holds the one shared parser, so every client and the daemon agree on what is a tag:

  • parseHappyReferences(markdown, { streaming? }) splits text into text and reference segments. Tags in code spans, fenced code, or after a backslash stay text.
  • replaceHappyReferences(markdown, replace, { streaming? }) substitutes each tag in the Markdown source before a Markdown parser sees it. Tags are never Markdown HTML, so substitute them instead of reading HTML nodes.
  • With streaming: true, both functions drop a trailing incomplete tag such as <happy-arti. happyReferencePartialTagLength(markdown) reports how many characters that is.
  • renderHappyReferencesAsMarkdown(markdown, resolve) is the plain-Markdown fallback. Each tag becomes its escaped label, or happyReferencePlaceholder(reference) when resolve returns nothing.
  • parseHappyReferenceTag(tag) parses exactly one tag. formatHappyReferenceTag(reference) writes the canonical one. isKnownHappyReferenceKind(kind) tells which kinds to resolve.
  • escapeMarkdownText(text) is the escaping the fallback uses for labels.

Show a resolved reference as a link whose text is the live title. Indicate a busy entity on the link. Show unknown kinds and unresolvable references as inert placeholder text.

File links

A file outside the reader's own workspace is linked as an ordinary Markdown link to a happy-file URL, as API.md specifies under "File links": [Foo.tsx:42](happy-file://agents/<agentId>/packages/app/Foo.tsx#L42-L50) or [README](happy-file://workspaces/<workspaceId>/README.md).

  • parseHappyFileLink(url) returns a HappyFileLink, { target, path, lines? }, or undefined for anything malformed. target is { type: "agent", agentId } or { type: "workspace", workspaceId }; path is decoded and relative to the workspace root; lines is inclusive, so #L42 is { start: 42, end: 42 }.
  • formatHappyFileLink(link) writes the canonical URL and throws a TypeError for an invalid link. happyFileLinkSchema is the TypeBox schema, and HAPPY_FILE_LINK_SCHEME and MAX_HAPPY_FILE_LINK_LENGTH are the scheme and the longest URL.
  • renderHappyFileLinksAsText(markdown) is the fallback for surfaces that cannot open file links: each inline link or image to a happy-file URL becomes its label. Links in code stay literal. The general form is replaceHappyFileLinks(markdown, replace).

Resolve an agent link through GET /v0/agents/:agentId to its workspaceId, then read the file from that workspace. Open it read-only inside the reader's own conversation, and show the usual "could not open" treatment for a malformed or unresolvable link. Never hand a happy-file URL to the operating system or a browser.