diff --git a/README.md b/README.md index d7a378a..a2992c4 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ Product docs and guides served directly in the dashboard with navigation, module Shared chat room where the user and multiple LLM instances (Claude Code, Cursor, Codex, etc.) communicate via @mention addressing. The server assigns each participant a unique memorable name. Messages are delivered to LLMs via PTY injection into their shell panes. +On top of free-form chat, **pipes** orchestrate multi-stage work across agents: linear hand-offs, fan-out, and synthesizer aggregation. Each stage has a leased assignment; agents read their entitled input with `pipe_read_output` and return results with `pipe_submit`. Message history is persisted per-project as JSONL, and pipe messages are dual-written to per-pipe JSONL files for efficient scoped reads. + ### And More | Module | Type | What it does | @@ -280,15 +282,21 @@ Shared chat room where the user and multiple LLM instances (Claude Code, Cursor,
-Chat — 5 tools +Chat — 11 tools | Tool | Description | |------|-------------| | `chat_join` | Join the chat room; pass a live `paneId` from `DEVGLIDE_PANE_ID` with `submitKey: "cr"` (default, correct for all known clients) | | `chat_leave` | Leave the chat room | -| `chat_send` | Send a message (use @mentions to target recipients) | +| `chat_send` | Send a message (delivery resolved from `to` plus body @mentions; use `@all` to broadcast) | | `chat_read` | Read message history with limit, since, and topic filters | | `chat_members` | List active participants with pane link status | +| `chat_status` | Check your connection status and diagnostics (debug delivery issues) | +| `pipe_submit` | Submit your output for a pipe stage (use instead of `chat_send` for a `#pipe-` prompt) | +| `pipe_read_output` | Read the stage input you are entitled to (prior stage output, prompt, or aggregated fan-out) | +| `pipe_get_assignment` | Inspect assignment metadata for a pipe (role, stage, lease, deadline) | +| `pipe_list_assignments` | List your active and pending pipe assignments with lease status | +| `pipe_status` | Get detailed pipe status: slot states, leases, timing, dead-letter entries |
diff --git a/bin/claude-md-template.js b/bin/claude-md-template.js index 9e5bccd..658a831 100644 --- a/bin/claude-md-template.js +++ b/bin/claude-md-template.js @@ -102,6 +102,13 @@ Messages are delivered to LLMs via PTY injection when linked to a shell pane. - \`chat_send\` — send a message (delivery goes to recipients resolved from \`to\` plus body @mentions; use \`@all\` to broadcast; LLM messages with no recipients in either field are persisted but not PTY-delivered) - \`chat_read\` — read message history (supports \`limit\`, \`since\` filters) - \`chat_members\` — list active participants with pane link status +- \`chat_status\` — check your connection status and diagnostics (debug delivery issues, verify session health) +- \`pipe_submit\` — submit your output for a pipe stage (use instead of \`chat_send\` when responding to a \`#pipe-\` prompt) +- \`pipe_read_output\` — read the stage input you are entitled to (prior stage output, original prompt, or aggregated fan-out outputs) +- \`pipe_get_assignment\` — inspect assignment metadata for a pipe (role, stage, lease status, deadline) +- \`pipe_list_assignments\` — list your active and pending pipe assignments with lease status and deadlines +- \`pipe_status\` — detailed pipe status: slot states, active leases, timing breakdown, dead-letter entries +- **Pipes:** Pipes orchestrate multi-stage work across agents — linear hand-offs, fan-out, and synthesizer aggregation. Each stage has a leased assignment. When you receive a \`#pipe-\` prompt, read your entitled input with \`pipe_read_output\` and return your result with \`pipe_submit\` (not \`chat_send\`). \`chat_send\` rejects messages that start with or reference a running \`#pipe-\`. - **Name assignment:** The server derives your chat alias from \`name\` + pane number (e.g. "claude-1" for name "claude" on pane 1). The \`name\` param is the stable identity base — use a consistent agent label, not the backend model. Always use the \`name\` returned by \`chat_join\`. - **Targeted PTY delivery:** Delivery recipients are resolved from the \`to\` param plus any body @mentions. Use \`@all\` as an explicit broadcast token. LLM messages with no recipients in either \`to\` or body @mentions are persisted in history but not PTY-delivered to any agent terminal. - **Rules of Engagement:** On \`chat_join\`, you receive a \`rules\` field (markdown) defining when to respond vs. stay silent. **Follow these rules exactly.** Default: reply if @mentioned, or on a global user request only after your claim has been explicitly confirmed by the other active LLM participants. Do not let multiple LLMs answer the same global request uncoordinated. Rules can be customized per project. diff --git a/scripts/build-mcp.mjs b/scripts/build-mcp.mjs index ee5cadb..a8c9989 100644 --- a/scripts/build-mcp.mjs +++ b/scripts/build-mcp.mjs @@ -20,7 +20,11 @@ const servers = [ "documentation", ]; -const external = ["better-sqlite3", "node-pty"]; +// Native / optional packages must not be inlined by esbuild. nodejs-whisper pulls in +// whisper.cpp and is an optional STT dependency that may be absent or fail to install; +// the voice provider imports it lazily and degrades gracefully at runtime. Bundling it +// makes the build fail with "Could not resolve 'nodejs-whisper'" when it is not present. +const external = ["better-sqlite3", "node-pty", "nodejs-whisper"]; // CJS packages bundled into ESM need a real require() for Node built-ins const banner = `import { createRequire as __bundleCR } from "module"; const require = __bundleCR(import.meta.url);`; diff --git a/src/apps/shell/src/runtime/shell-config.test.ts b/src/apps/shell/src/runtime/shell-config.test.ts new file mode 100644 index 0000000..68005f2 --- /dev/null +++ b/src/apps/shell/src/runtime/shell-config.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { pickUnixShell, withDefaultPath } from './shell-config.js'; + +describe('pickUnixShell', () => { + it('returns $SHELL when it exists', () => { + const exists = (p: string) => p === '/opt/homebrew/bin/fish'; + expect(pickUnixShell(exists, '/opt/homebrew/bin/fish')).toBe('/opt/homebrew/bin/fish'); + }); + + it('ignores $SHELL when the path does not exist', () => { + const exists = (p: string) => p === '/bin/zsh'; + expect(pickUnixShell(exists, '/nonexistent/shell')).toBe('/bin/zsh'); + }); + + it('falls back to an ABSOLUTE path (never a bare name) when $SHELL is unset', () => { + // Regression: bare "bash" makes posix_spawnp fail when the child env has no PATH (macOS daemon/GUI launch). + const exists = (p: string) => p === '/bin/zsh'; + const result = pickUnixShell(exists, undefined); + expect(result.startsWith('/')).toBe(true); + expect(result).toBe('/bin/zsh'); + }); + + it('prefers zsh, then bash, then sh', () => { + expect(pickUnixShell((p) => ['/bin/zsh', '/bin/bash', '/bin/sh'].includes(p), undefined)).toBe('/bin/zsh'); + expect(pickUnixShell((p) => ['/bin/bash', '/bin/sh'].includes(p), undefined)).toBe('/bin/bash'); + expect(pickUnixShell((p) => p === '/bin/sh', undefined)).toBe('/bin/sh'); + }); + + it('never returns a bare command even when nothing is found', () => { + const result = pickUnixShell(() => false, undefined); + expect(result.startsWith('/')).toBe(true); + }); +}); + +describe('withDefaultPath', () => { + it('injects a sane PATH when the env has none', () => { + const env = withDefaultPath({ TERM: 'xterm-256color' }); + expect(env.PATH).toBeTruthy(); + expect(env.PATH).toContain('/bin'); + }); + + it('preserves an existing PATH', () => { + const env = withDefaultPath({ PATH: '/custom/bin' }); + expect(env.PATH).toBe('/custom/bin'); + }); +}); diff --git a/src/apps/shell/src/runtime/shell-config.ts b/src/apps/shell/src/runtime/shell-config.ts index b9464cd..7f68083 100644 --- a/src/apps/shell/src/runtime/shell-config.ts +++ b/src/apps/shell/src/runtime/shell-config.ts @@ -5,6 +5,35 @@ export type { ShellConfig }; const ENV_ALLOWLIST_UNIX = ['HOME', 'PATH', 'USER', 'SHELL', 'LANG', 'LC_ALL', 'SSH_AUTH_SOCK']; +/** Absolute shells tried, in order, when $SHELL is unset/missing. zsh is the macOS default since Catalina. */ +const UNIX_SHELL_CANDIDATES = ['/bin/zsh', '/bin/bash', '/bin/sh', '/usr/bin/zsh', '/usr/bin/bash', '/usr/bin/sh']; + +/** Conservative default PATH for spawn envs launched without one (daemon/GUI launch). */ +const DEFAULT_UNIX_PATH = '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'; + +/** + * Ensure a spawn env has a PATH. A macOS daemon or GUI-launched process can have a sparse + * environment with no PATH; without it node-pty's posix_spawnp cannot locate a bare command + * and fails. Pure/testable — does not read process.env. + */ +export function withDefaultPath(env: Record): Record { + if (env.PATH) return env; + return { ...env, PATH: DEFAULT_UNIX_PATH }; +} + +/** + * Resolve a macOS/Linux shell to an ABSOLUTE path. Returning a bare name ("bash") makes + * posix_spawnp depend on PATH being present in the child env, which fails under daemon/GUI + * launch. Pure/testable — caller injects the existence check and $SHELL value. + */ +export function pickUnixShell(exists: (p: string) => boolean, shellEnv: string | undefined): string { + if (shellEnv && exists(shellEnv)) return shellEnv; + for (const sh of UNIX_SHELL_CANDIDATES) { + if (exists(sh)) return sh; + } + return '/bin/sh'; +} + export function safeEnv(extra: Record = {}): Record { if (process.platform === 'win32') { const env: Record = {}; @@ -18,18 +47,18 @@ export function safeEnv(extra: Record = {}): Record = {