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
101 changes: 93 additions & 8 deletions bin/claudex.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
#!/usr/bin/env bash
# claudex — open a fresh tmux session with `claude` in one window and
# `expediter` in another, then attach (or switch) to it.
# claudex — open a fresh tmux session with `expediter` and `claude` in two
# side-by-side panes, then attach (or switch) to it.
#
# Run from anywhere via the ~/.local/bin/claudex shim installed by install.sh.
# The shim sets EXPEDITER_HOME but this script doesn't need it — it only needs
# `claude` and `expediter` to be on PATH (which install.sh guarantees).
#
# Subcommands:
# claudex default: expediter (left) + claude (right) panes
# claudex uno newbie onboarding (daemon + QR + 4 numbered steps)
# claudex tour sonnet fresh tmux session with the Sonnet explainer
# claudex tour haiku fresh tmux session with the Haiku haiku-writer

set -u

Expand All @@ -23,20 +29,99 @@ if ! command -v expediter >/dev/null 2>&1; then
exit 1
fi

# --- subcommand dispatch --------------------------------------------------
# claudex (no args) → existing behavior (claude + expediter)
# claudex uno → newbie onboarding (daemon + QR + 4 steps)
# claudex tour sonnet → fresh tmux session with Sonnet explainer
# claudex tour haiku → fresh tmux session with Haiku haiku-writer
# anything else → usage + exit 1

case "${1:-}" in
"")
# Fall through to existing claudex behavior below.
;;
uno)
# Hand off to expediter with the four newbie-onboarding steps.
# expediter --steps splits on `|` and prints each as a numbered line
# beneath the QR. The user manually opens two new tabs and runs
# `claudex tour sonnet` / `claudex tour haiku` in each.
exec expediter --steps "Scan the QR with your phone.|Open a new tab (Cmd+T) and run: claudex tour sonnet|Open another new tab (Cmd+T) and run: claudex tour haiku|After running all these steps, watch your phone!"
;;
tour)
MODEL="${2:-}"
case "$MODEL" in
sonnet|haiku) ;;
"")
echo "claudex tour: missing model. Usage: claudex tour [sonnet|haiku]" >&2
exit 1
;;
*)
echo "claudex tour: unknown model '$MODEL'. Usage: claudex tour [sonnet|haiku]" >&2
exit 1
;;
esac
# Tour prompts live in $EXPEDITER_HOME/bin/uno_prompts/<model>.txt so
# they can be edited without touching shell-quoting in this script.
# The shim at ~/.local/bin/claudex sets EXPEDITER_HOME before exec'ing
# this file.
if [ -z "${EXPEDITER_HOME:-}" ] || [ ! -d "$EXPEDITER_HOME" ]; then
echo "claudex tour: EXPEDITER_HOME is not set. Re-run install.sh." >&2
exit 1
fi
PROMPT_TXT="$EXPEDITER_HOME/bin/uno_prompts/$MODEL.txt"
if [ ! -f "$PROMPT_TXT" ]; then
echo "claudex tour: prompt file not found at $PROMPT_TXT" >&2
exit 1
fi
PROMPT="Read $PROMPT_TXT and respond per its contents."
# Shell-escape the prompt for safe consumption by tmux's /bin/sh -c.
# printf '%q' produces a re-quoted form that survives one more layer
# of shell parsing (tmux runs the new-session command via /bin/sh -c).
TOUR_SESSION="claudex-tour-$MODEL-$(date +%s)"
QUOTED_PROMPT=$(printf '%q' "$PROMPT")
tmux new-session -d -s "$TOUR_SESSION" -n claude -c "$PWD" "claude --model $MODEL $QUOTED_PROMPT"
if [ -n "${TMUX:-}" ]; then
tmux switch-client -t "$TOUR_SESSION"
else
tmux attach-session -t "$TOUR_SESSION"
fi
exit 0
;;
*)
echo "claudex: unknown subcommand '$1'" >&2
echo "Usage: claudex [uno | tour sonnet | tour haiku]" >&2
exit 1
;;
esac

# --- default behavior (no subcommand) -------------------------------------
# Generate a unique session name. The seconds-since-epoch suffix keeps each
# claudex invocation isolated, so running it twice gives you two independent
# sessions instead of clobbering the first.
SESSION="claudex-$(date +%s)"

# Create the session detached with the claude window first so it's window 1.
tmux new-session -d -s "$SESSION" -n claude -c "$PWD" 'claude'
tmux new-window -t "$SESSION:" -n expediter -c "$PWD" 'expediter'
# One window split into two side-by-side panes: expediter (QR) on the left,
# claude on the right. Side-by-side panes are friendlier to first-time users
# than two windows behind tmux navigation — both processes are visible from
# the moment they attach, no Ctrl-b n required to find the QR.
# tmux's -h flag splits "horizontally" by tmux convention, which actually
# produces panes that sit side-by-side (the new pane is to the right of the
# original). -v would stack them vertically.
#
# `; exec $SHELL` on the expediter pane keeps it open after expediter exits.
# When the daemon is already up, `expediter` prints the QR and returns
# immediately — without this, the pane would close and the user would see
# only the claude pane.
tmux new-session -d -s "$SESSION" -n main -c "$PWD" "expediter; exec \${SHELL:-bash}"
tmux split-window -t "$SESSION:main" -h -c "$PWD" 'claude'

# Select the claude window so the user lands on it (vs. the expediter logs).
tmux select-window -t "$SESSION:claude"
# After the split, pane 0 is expediter (left) and pane 1 is claude (right).
# Land the user on the claude pane so they can start typing immediately; the
# QR remains visible to their left.
tmux select-pane -t "$SESSION:main.1"

# If we're already inside tmux, switch-client; otherwise attach. Either way
# the user ends up looking at the new session's claude window.
# the user ends up looking at the new session with both panes visible.
if [ -n "${TMUX:-}" ]; then
tmux switch-client -t "$SESSION"
else
Expand Down
19 changes: 18 additions & 1 deletion bin/expediter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@ const PRINT_URL = process.argv.includes('--print-url');
const SHOW_HELP = process.argv.includes('--help') || process.argv.includes('-h');
const TITLE_IDX = process.argv.indexOf('--title');
const TITLE_VALUE = TITLE_IDX >= 0 ? process.argv[TITLE_IDX + 1] : null;
// --steps "<s1>|<s2>|..." — opt-in numbered-steps list appended below the QR.
// Used by `claudex uno` to print newbie-onboarding instructions. Plain
// `expediter` without --steps never prints steps. Steps are pipe-delimited;
// each step renders on its own line prefixed with "<n>. " (1-indexed).
const STEPS_IDX = process.argv.indexOf('--steps');
const STEPS_RAW = STEPS_IDX !== -1 ? process.argv[STEPS_IDX + 1] : undefined;

if (SHOW_HELP) {
console.log('Usage: expediter [--print-url] [--title default|haiku] [--help]');
console.log(
'Usage: expediter [--print-url] [--title default|haiku] [--steps "<s1>|<s2>|..."] [--help]'
);
console.log('');
console.log(' --print-url Also print the tethered URL as text (default: QR only).');
console.log(' Use this only if your phone cannot scan the QR — the URL');
Expand All @@ -29,6 +37,8 @@ if (SHOW_HELP) {
console.log(' /rename), with a whimsical name as fallback. "haiku" uses');
console.log(' the LLM-generated caveman summary. Writes to');
console.log(' ~/.expediter/config.json.');
console.log(' --steps Pipe-delimited list of numbered steps to print below the QR.');
console.log(' Opt-in; used by `claudex uno` for newbie-onboarding.');
console.log(' --help, -h Show this message.');
process.exit(0);
}
Expand Down Expand Up @@ -179,6 +189,13 @@ async function printAccess() {
console.log(' WARNING: the URL above contains the session token and will stay in');
console.log(' your terminal scrollback. Restart the daemon to invalidate it.');
}
if (STEPS_RAW) {
console.log('');
const steps = STEPS_RAW.split('|');
steps.forEach((step, i) => {
console.log(`${i + 1}. ${step}`);
});
}
}

// --- main ---
Expand Down
1 change: 1 addition & 0 deletions bin/uno_prompts/haiku.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Write a haiku about a positive, collaborative human-AI future.
32 changes: 32 additions & 0 deletions bin/uno_prompts/sonnet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
You're greeting a new user who just ran `claudex uno` for the first time. They're new to both the expediter and tmux. Output plain terminal text with three numbered sections.

Format:

1. What is the expediter?

A few short sentences, warm and conversational. The real value: it reduces the friction of getting to any agent session. When an agent needs you — a permission request, or an update — your phone alerts you, and tapping the ticket jumps you straight to that session in your Terminal. Keeps you actively in the loop with all your running agents at once, without hunting through tabs for which one needs you. Phone and Mac need to be on the same network. Do NOT frame it as "avoid walking back to your desk" or "watch agents from your phone" — frame it as "actively stay in the loop, get to the right session fast".

2. What is tmux?

Really ELI5 — explain it like to a curious 5-year-old. tmux is a tab manager for your terminal. Use a friendly analogy in one or two sentences. Then present two lists.

First list — sessions/windows/panes. Each line is "Term: explanation". Format like:

Sessions: separate workspaces, like different projects.
Windows: tabs within a session.
Panes: splits inside a window, for seeing things side-by-side.

Second list — handy hotkeys. Each line is "command: what it does". Format like:

Ctrl-b c: opens a new window.
Ctrl-b n: jumps to the next window.
Ctrl-b d: detaches from the session (keeps running in the background).
Ctrl-b &: closes the current window.

3. How to use the expediter

For any future session, as long as you run `expediter` and are interacting with Claude Code inside tmux, you'll be able to monitor those agents from your phone. You can also just type `claudex` to open both the expediter and Claude Code at once inside tmux.

After section 3, a short closing line: If you have any questions, I recommend asking Claude first — Claude knows a lot. Or feel free to message the developer at hi@givemeanudge.com or @akashbert on X.

No markdown formatting (no bold, italics, bullets, or hash headers). Just plain text with numbered section titles and the line-by-line list format shown above. Conversational tone. Keep each section brief.
7 changes: 4 additions & 3 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ esac
# --- done ------------------------------------------------------------------

printf '\n%s✦%s Expediter is ready!\n\n' "$GREEN" "$RESET"
printf 'Few ways to use expediter:\n\n'
printf 'expediter start the daemon and print the QR for linking your phone\n'
printf 'claudex open tmux with claude + expediter side-by-side\n'
printf '%sFew ways to use expediter:%s\n\n' "$BOLD" "$RESET"
printf ' %sexpediter%s start the daemon and print the QR for linking your phone\n' "$BOLD" "$RESET"
printf ' %sclaudex%s open tmux with claude + expediter side-by-side\n' "$BOLD" "$RESET"
printf ' %sclaudex uno%s new to tmux or Claude Code? start here\n\n' "$BOLD" "$RESET"
4 changes: 4 additions & 0 deletions src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Expediter" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png" />
<link rel="apple-touch-icon" href="/icon-192.png" />
<script>
// Grab the per-session token from the URL fragment (#<token>), stash it
Expand Down
6 changes: 0 additions & 6 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
<script lang="ts">
import favicon from '$lib/assets/favicon.svg';

let { children } = $props();
</script>

<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>

{@render children()}
Loading