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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Run this once in your Mac's Terminal:
curl -fsSL https://raw.githubusercontent.com/HeftyKoo/AgentOnWeb/main/scripts/install.sh | bash
```

The installer sets up the local terminal, automatic startup and Chrome connection. It detects Codex, Claude Code and Pi on `PATH`, then shows a terminal menu to choose all or some detected CLI integrations, or skip them. Existing CLI settings are preserved. You can also choose explicitly with `aow setup --agents codex,claude`, `--agents all` or `--agents none`.
The installer sets up the local terminal, automatic startup and Chrome connection. It detects Codex, Claude Code and Pi on `PATH`, then shows a checkbox menu: Up/Down to move, Space to toggle, A to select/clear all, I to invert, and Enter to confirm. Clear all to skip integrations. Existing CLI settings are preserved. You can also choose explicitly with `aow setup --agents codex,claude`, `--agents all` or `--agents none`.

The install command works from zsh, bash and fish. When Node's command directory is writable and already on PATH, the installer adds an `aow` entry there, so the same terminal can run `aow service` immediately without sourcing a profile. New zsh/bash windows and detected fish installations also get `aow` on their PATH. If that directory is protected or has an unrelated `aow` command, use `"$HOME/.local/bin/aow" service` directly.

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ AgentOnWeb 将你的编程工作区带到你当前打开的页面上。一边看
curl -fsSL https://raw.githubusercontent.com/HeftyKoo/AgentOnWeb/main/scripts/install.sh | bash
```

安装器会配置本机终端、登录自动启动和 Chrome 连接,先检测 `PATH` 中的 Codex / Claude Code / Pi,再显示终端交互菜单,让你选择安装全部、部分已检测到的 CLI 集成,或跳过。已有的 CLI 设置会保留。也可以用 `aow setup --agents codex,claude`、`--agents all` 或 `--agents none` 显式选择。
安装器会配置本机终端、登录自动启动和 Chrome 连接,先检测 `PATH` 中的 Codex / Claude Code / Pi,再显示多选框菜单,默认勾选全部已检测到的 CLI。方向键移动,空格切换勾选,A 全选/全不选,I 反选,回车确认;清空勾选可只配置终端服务,Ctrl+C 或 Esc 取消。已有的 CLI 设置会保留。也可以用 `aow setup --agents codex,claude`、`--agents all` 或 `--agents none` 显式选择。

安装命令兼容 zsh、bash 和 fish。如果 Node 命令目录已在 PATH 中且可写,安装器会在其中创建 `aow` 入口,同一个终端安装后即可运行 `aow service`,无需 source。新开的 zsh/bash 窗口及检测到的 fish 也会配置 `aow` 路径。如果该目录受保护或已有其他程序的 `aow` 命令,可直接运行 `"$HOME/.local/bin/aow" service`。

Expand Down
2 changes: 1 addition & 1 deletion docs/one-command-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ For an already-open terminal, the installer also creates a link to the installed

## Choose CLI integrations

By default, both the installer and `aow setup` show a numbered terminal menu for the detected CLIs. Enter a number, several comma-separated numbers (for example `1,2`), or CLI names (for example `codex,claude`). Press Enter or type `all` to choose every detected CLI; enter `0` or `none` to configure only the terminal service. The installer reads the choice from your terminal even when run through `curl | bash`. Cancel with Control+C before integration/service configuration starts.
By default, both the installer and `aow setup` show a checkbox menu for the detected CLIs. All detected CLIs start selected. Use **Up/Down** to move, **Space** to toggle a checkbox, **A** to select/clear all, **I** to invert, and **Enter** to confirm. Clear all checkboxes to configure only the terminal service. The installer reads the choice from your terminal even when run through `curl | bash`. Cancel with Control+C or Esc before integration/service configuration starts.

To choose explicitly without an interactive menu:

Expand Down
2 changes: 1 addition & 1 deletion docs/store-site/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ Terminal host 0.4.1 checks the stable npm version daily without installing code

## Setup, notifications and removal

On macOS + Chrome, `aow setup` configures the service and native pairing, and shows a terminal menu for the Codex/Claude Code/Pi integrations detected on `PATH`. Select all, comma-separated numbers/names, or `0` to skip integrations. For unattended setup use `--agents codex,claude`, `--agents all` or `--agents none`; missing CLIs are always skipped. Review AgentOnWeb hooks in your CLI's `/hooks`, then launch a new Codex or Claude Code session in a new host terminal for session notifications. If an older host is running, finish terminal work, run `aow service uninstall`, then `aow setup`. Before removing the npm package run `aow uninstall`. See [setup, upgrade and uninstall](https://github.com/HeftyKoo/AgentOnWeb/blob/main/docs/one-command-setup.md).
On macOS + Chrome, `aow setup` configures the service and native pairing, and shows a checkbox menu for the Codex/Claude Code/Pi integrations detected on `PATH`. Use Up/Down to move, Space to toggle, A to select/clear all, I to invert, and Enter to confirm. Clear all to skip integrations. For unattended setup use `--agents codex,claude`, `--agents all` or `--agents none`; missing CLIs are always skipped. Review AgentOnWeb hooks in your CLI's `/hooks`, then launch a new Codex or Claude Code session in a new host terminal for session notifications. If an older host is running, finish terminal work, run `aow service uninstall`, then `aow setup`. Before removing the npm package run `aow uninstall`. See [setup, upgrade and uninstall](https://github.com/HeftyKoo/AgentOnWeb/blob/main/docs/one-command-setup.md).
4 changes: 3 additions & 1 deletion packages/terminal-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Install [the Chrome extension](https://chromewebstore.google.com/detail/agentonw
curl -fsSL https://raw.githubusercontent.com/HeftyKoo/AgentOnWeb/main/scripts/install.sh | bash
```

The installer configures the terminal service and Chrome connection. It detects `codex`, `claude` and `pi` executables on `PATH` and asks which notification integrations to install in a numbered terminal menu, including when run through `curl | bash`. Choose all detected CLIs, comma-separated numbers/names, or `0` to skip integrations. Missing CLIs are always skipped. Open a website, click AgentOnWeb and run:
The installer configures the terminal service and Chrome connection. It detects `codex`, `claude` and `pi` executables on `PATH` and asks which notification integrations to install in a checkbox menu, including when run through `curl | bash`. Use Up/Down to move, Space to toggle, A to select/clear all, I to invert, and Enter to confirm. All detected CLIs start selected; clear all to configure the service only. Missing CLIs are always skipped. Open a website, click AgentOnWeb and run:

```sh
cd ~/your-project
Expand Down Expand Up @@ -87,3 +87,5 @@ npm uninstall --global --prefix "$HOME/.local" @agentonweb/terminal-host
If you installed through npm directly, uninstall from your original npm prefix instead. User settings, CLI history and backups remain.

[Setup and troubleshooting](https://github.com/HeftyKoo/AgentOnWeb/blob/main/docs/one-command-setup.md) · [Agent notifications](https://github.com/HeftyKoo/AgentOnWeb/blob/main/docs/agent-notifications.md) · [Source development](https://github.com/HeftyKoo/AgentOnWeb/blob/main/docs/development.md)

The human-facing CLI uses a compact Matrix-inspired green theme, grouped help and status labels. Run `aow --help` (or `aow help`) and `aow <command> --help` for details. Help adapts to terminal width; redirected output is plain text. Set `NO_COLOR=1` to disable color.
115 changes: 115 additions & 0 deletions packages/terminal-host/src/cli-help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { terminalUi } from './cli-ui.js';

type HelpSection = { title: string; rows: readonly (readonly [string, string])[] };
type Help = { usage: string; summary?: string; sections: HelpSection[] };
export const SETUP_USAGE = 'Usage: aow setup [--agents all|none|codex,claude,pi] [--extension-id CHROME_EXTENSION_ID]';
const topics: Record<string, Help> = {
main: {
usage: 'Usage: aow <command> [options]',
summary: 'Your local terminal, connected to the web.',
sections: [
{ title: 'CONNECT', rows: [
['setup', 'Configure the service and choose agent integrations.'],
['terminal', 'Run a foreground terminal host (also the default command).'],
['uninstall', 'Remove AgentOnWeb integrations and the local service.'],
] },
{ title: 'SERVICE', rows: [
['service [open]', 'Open the private browser connection page.'],
['service install', 'Start the service automatically at login.'],
['service status', 'Show the service, host version and active terminals.'],
['service restart [--force]', 'Restart the host; --force ends active tasks.'],
['service uninstall', 'Remove automatic startup and stop service terminals.'],
] },
{ title: 'AGENT INTEGRATIONS', rows: [
['codex-hooks install|uninstall', 'Manage Codex session hooks.'],
['claude-hooks install|uninstall', 'Manage Claude Code session hooks.'],
['pi-extension install|uninstall', 'Manage the Pi extension (Pi 1.1+).'],
] },
{ title: 'MAINTENANCE', rows: [
['check-update', 'Check for a newer stable npm version.'],
['check-update --enable|--disable', 'Enable or disable automatic update checks.'],
['update [--force]', 'Upgrade the CLI and restart the macOS service.'],
['--version', 'Print the installed version.'],
['-h, --help, help', 'Show this help.'],
] },
{ title: 'QUICK START', rows: [
['aow setup', 'Choose integrations with a checkbox menu.'],
['aow terminal', 'Run the terminal host in the foreground.'],
['aow setup --agents codex,pi', 'Select integrations without prompting.'],
['aow service status', 'Check the running service.'],
] },
],
},
setup: {
usage: SETUP_USAGE,
summary: 'Detect CLIs on PATH, then ask which integrations to install with a checkbox menu.',
sections: [
{ title: 'KEYS', rows: [
['Up / Down', 'Move between detected agents.'], ['Space', 'Toggle the focused checkbox.'],
['A / I', 'Select or clear all / invert selection.'], ['Enter', 'Confirm; clear all for the terminal service only.'],
['Ctrl+C / Esc', 'Cancel before installing integrations or configuring the service.'],
] },
{ title: 'OPTIONS', rows: [
['--agents codex,claude,pi', 'Select a comma-separated subset without prompting.'],
['--agents all', 'Select every detected CLI without prompting.'],
['--agents none', 'Configure the terminal service only.'],
['--extension-id ID', 'Also allow this Chrome extension ID; may be repeated.'],
] },
{ title: 'NOTES', rows: [
['Detection', 'Missing CLIs are always skipped. Selection does not remove existing integrations.'],
['Terminal', 'Works with curl | bash through /dev/tty. Use --agents when no interactive terminal is available.'],
] },
],
},
service: {
usage: 'Usage: aow service <action> [options]',
summary: 'Manage the macOS login service (default: open).',
sections: [{ title: 'ACTIONS', rows: [
['open', 'Open the private browser connection page.'],
['install [--no-open]', 'Install automatic startup; optionally skip opening the browser.'],
['status', 'Show the loaded service, installed/running versions and terminals.'],
['restart [--force]', 'Restart after closing terminals; --force ends their tasks.'],
['uninstall', 'Remove automatic startup and stop service terminals.'],
] }],
},
'check-update': {
usage: 'Usage: aow check-update [--enable|--disable]',
summary: 'Check the stable npm version. Explicit checks still work when automatic checks are disabled.',
sections: [{ title: 'OPTIONS', rows: [
['--enable', 'Enable automatic update checks.'], ['--disable', 'Disable automatic update checks.'],
] }],
},
update: {
usage: 'Usage: aow update [--force]',
summary: 'Install the newer stable npm version into the existing prefix and restart the macOS service. Close terminals first.',
sections: [{ title: 'OPTIONS', rows: [['--force', 'End active terminal tasks before restarting.']] }],
},
terminal: {
usage: 'Usage: aow terminal', summary: 'Run the terminal host in the foreground. Keep this process running while using your browser terminal.',
sections: [{ title: 'SERVICE', rows: [['aow setup', 'Configure automatic startup on macOS + Chrome.']] }],
},
uninstall: {
usage: 'Usage: aow uninstall', summary: 'Remove hooks, notification forwarding, the native bridge and the local service.',
sections: [{ title: 'RETAINED DATA', rows: [['Local data', 'Backups, browser grants, pairing receipts and CLI history are retained.']] }],
},
};
for (const [command, label] of [['codex-hooks', 'Codex hooks'], ['claude-hooks', 'Claude Code hooks'], ['pi-extension', 'Pi extension']] as const) {
topics[command] = {
usage: `Usage: aow ${command} install|uninstall`,
sections: [{ title: label.toUpperCase(), rows: [
['install', `Install AgentOnWeb ${label}; preserve existing settings.`],
['uninstall', `Remove AgentOnWeb ${label}; preserve other settings.`],
] }],
};
}

export function hasHelp(topic: string) { return Object.hasOwn(topics, topic); }

export function formatHelp(topic = 'main', ui = terminalUi()) {
const help = topics[topic]!;
return [ui.banner(topic === 'main' ? 'COMMAND INTERFACE' : topic.toUpperCase()), '',
ui.wrap(help.usage), ...(help.summary ? ['', ui.wrap(help.summary)] : []),
...help.sections.flatMap(section => ['', ui.section(section.title), ui.rows(section.rows)]),
...(topic === 'main' ? ['', ui.wrap('Run aow <command> --help for options and keyboard controls.')] : []),
].join('\n');
}
30 changes: 30 additions & 0 deletions packages/terminal-host/src/cli-ui.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { stripVTControlCharacters } from 'node:util';
import { expect, it } from 'vitest';
import { terminalUi } from './cli-ui.js';
import { formatHelp } from './cli-help.js';

const output = (columns: number, isTTY = true) => ({ write: () => true, columns, isTTY });

it.each([24, 40, 60, 80, 120])('keeps every help line within a %s-column terminal', columns => {
const ui = terminalUi(output(columns), { TERM: 'xterm-256color' });
for (const topic of ['main', 'setup', 'service', 'update', 'check-update', 'codex-hooks', 'claude-hooks', 'pi-extension']) {
const help = stripVTControlCharacters(formatHelp(topic, ui));
for (const line of help.split('\n')) expect(line.length, `${topic}: ${line}`).toBeLessThan(columns);
}
});

it('groups commands with descriptions and examples', () => {
const help = formatHelp('main', terminalUi(output(80), {}));
for (const title of ['CONNECT', 'SERVICE', 'AGENT INTEGRATIONS', 'MAINTENANCE', 'QUICK START']) expect(help).toContain(`[ ${title} ]`);
expect(help).toContain('aow setup --agents codex,pi');
expect(help).toContain('aow terminal');
expect(help).toContain('aow <command> --help');
});

it('uses ANSI green on terminals and plain text for pipes, NO_COLOR and TERM=dumb', () => {
expect(terminalUi(output(80), {}).banner()).toContain('\x1b[1;92m');
for (const [isTTY, env] of [[false, {}], [true, { NO_COLOR: '' }], [true, { NO_COLOR: '1', FORCE_COLOR: '1' }], [true, { TERM: 'dumb' }], [true, { FORCE_COLOR: '0' }]] as const) {
const help = formatHelp('main', terminalUi(output(80, isTTY), env));
expect(help).not.toContain('\x1b');
}
});
67 changes: 67 additions & 0 deletions packages/terminal-host/src/cli-ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { Writable } from 'node:stream';
import { VERSION } from './version.js';

type TerminalOutput = Pick<Writable, 'write'> & { isTTY?: boolean; columns?: number };
type Tone = 'green' | 'muted' | 'warning' | 'error';
type Status = 'ok' | 'info' | 'skip' | 'warn';
const colors = { green: '1;92', muted: '32', warning: '33', error: '31' };

/** Styling belongs at the human output boundary; protocol/version output stays raw. */
export function terminalUi(output: TerminalOutput = process.stdout, env = process.env) {
const color = env.NO_COLOR === undefined && env.TERM !== 'dumb'
&& (env.FORCE_COLOR !== undefined ? env.FORCE_COLOR !== '0' : Boolean(output.isTTY));
const width = Math.max(1, Math.min(output.columns || 80, 100) - 1);
const paint = (text: string, tone: Tone = 'green') => color ? `\x1b[${colors[tone]}m${text}\x1b[0m` : text;
const wrap = (text: string, indent = ''): string => {
const prefix = indent.slice(0, Math.max(0, width - 1));
const size = Math.max(1, width - prefix.length);
return text.split('\n').flatMap(line => {
const lines: string[] = [];
let rest = line;
while (rest.length > size) {
const space = rest.lastIndexOf(' ', size);
const end = space > 0 ? space : size;
lines.push(prefix + rest.slice(0, end));
rest = rest.slice(end).trimStart();
}
lines.push(prefix + rest);
return lines;
}).join('\n');
};
return {
width, paint, wrap,
banner: (context?: string) => [
paint(wrap(`AOW // AGENTONWEB v${VERSION}`)),
...(context ? [paint(wrap(context), 'muted')] : []),
paint('-'.repeat(Math.min(width, 62)), 'muted'),
].join('\n'),
section: (title: string) => paint(wrap(`[ ${title} ]`)),
rows: (rows: readonly (readonly [string, string])[]) => {
const column = Math.max(...rows.map(([command]) => command.length)) + 2;
return rows.map(([command, description]) => {
if (width < 78 || column > width / 2) {
return paint(wrap(command, ' ')) + '\n' + wrap(description, ' ');
}
const descriptionLines = wrap(description, ' '.repeat(column + 2)).split('\n');
descriptionLines[0] = ' ' + paint(command.padEnd(column)) + descriptionLines[0]!.slice(column + 2);
return descriptionLines.join('\n');
}).join('\n');
},
status: (kind: Status, text: string) => {
const tag = `[${kind}]`;
const lines = wrap(text, ' '.repeat(tag.length + 1)).split('\n');
lines[0] = paint(tag, kind === 'warn' ? 'warning' : kind === 'skip' ? 'muted' : 'green')
+ ' ' + lines[0]!.slice(tag.length + 1);
return lines.join('\n');
},
};
}

export function printStatus(kind: Status, text: string) {
console.log(terminalUi().status(kind, text));
}

export function printError(error: unknown) {
const ui = terminalUi(process.stderr);
console.error(ui.paint('aow:', 'error') + ' ' + (error instanceof Error ? error.message : String(error)));
}
Loading
Loading