feat(setup): ping agent before chat, detect stale service, auto-install Claude

Round-trip confirmation before first chat. After cli-agent wires up the
Terminal Agent, send `chat ping` through the CLI socket under a spinner
with 30s timeout (shared helper in setup/lib/agent-ping.ts, also used by
verify). Only after a real reply do we show "Your assistant is ready."
and enter the chat loop. Ping failures surface a targeted note
(socket_error vs no_reply) and skip the prompt — so users never type
into the void.

Checkout-mismatch detection. verify resolves the running service PID's
script path via `ps -p <pid> -o command=` and compares to projectRoot.
If the service is running from a sibling clone (common for developers
with multiple checkouts), SERVICE comes back as running_other_checkout
instead of running, AGENT_PING is skipped, and the failure note tells
the user exactly which bootout + bootstrap pair to run.

Native Claude Code install on demand. Only the subscription auth path
needs `claude`; the paste-token and paste-API-key paths don't. So
register-claude-token.sh now runs setup/install-claude.sh when `claude`
is missing (curl -fsSL https://claude.ai/install.sh | bash), then
prepends ~/.local/bin to PATH in-process so the rest of the script can
see the fresh binary.

Gutter-safe wrapping. wrapForGutter + dimWrap in lib/theme.ts hard-wrap
text to `process.stdout.columns - gutter` on word boundaries, measuring
visible length (ANSI-stripped). dimWrap applies the dim envelope per
line because clack resets styling at each line break when rendering
multi-line log content — a single outer dim() only colors the first
line. Applied to the long "why" notes before container + onecli, the
channel-skip info, the ping-failure note, and the checkout-mismatch
remediation.

Wordmark anchoring. printIntro always includes the NanoClaw wordmark in
the clack intro line, whether or not nanoclaw.sh already printed one in
bash. Worth ~1 line of redundancy so the brand stays visible at the top
of the clack session after bootstrap output scrolls out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-22 11:06:15 +03:00
parent 9b6e5b24a1
commit 72b7a72cbb
6 changed files with 389 additions and 78 deletions

View File

@@ -37,3 +37,66 @@ export function brandChip(s: string): string {
}
return k.bgCyan(k.black(k.bold(s)));
}
/**
* Wrap text so it fits inside clack's gutter without the terminal's soft
* wrap breaking the `│ …` bar on long lines. Works on a single string with
* embedded `\n`s; each logical line is wrapped independently.
*
* The `gutter` argument is the total horizontal overhead clack adds for
* the component the text lives in (e.g. 4 for `p.log.*`'s `│ ` prefix;
* 6-ish for `p.note`'s box). Caller picks it; we just subtract from
* `process.stdout.columns` and hard-wrap at word boundaries.
*/
export function wrapForGutter(text: string, gutter: number): string {
const cols = process.stdout.columns ?? 80;
const width = Math.max(30, cols - gutter);
return text
.split('\n')
.map((line) => wrapLine(line, width))
.join('\n');
}
/**
* Wrap + dim together. Needed instead of `k.dim(wrapForGutter(...))`
* because clack resets styling at each line break when rendering
* multi-line log content — a single outer dim envelope only colors the
* first line. Applying dim per-line gives each wrapped row its own
* `\x1b[2m…\x1b[0m` envelope so the whole block reads as one block.
*/
export function dimWrap(text: string, gutter: number): string {
return wrapForGutter(text, gutter)
.split('\n')
.map((line) => k.dim(line))
.join('\n');
}
const ANSI_RE = /\x1b\[[0-9;]*m/g;
function visibleLength(s: string): number {
return s.replace(ANSI_RE, '').length;
}
function wrapLine(line: string, width: number): string {
if (visibleLength(line) <= width) return line;
const words = line.split(' ');
const rows: string[] = [];
let cur = '';
let curLen = 0;
for (const word of words) {
const wLen = visibleLength(word);
if (curLen === 0) {
cur = word;
curLen = wLen;
} else if (curLen + 1 + wLen <= width) {
cur += ' ' + word;
curLen += 1 + wLen;
} else {
rows.push(cur);
cur = word;
curLen = wLen;
}
}
if (cur) rows.push(cur);
return rows.join('\n');
}