mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-12 18:11:51 +08:00
feat(setup): rewrite copy for first-time users + split auth flow
Content pass: every user-facing line is rewritten from the perspective
of someone trying NanoClaw for the first time. Phase labels and devops
framing are gone. Examples:
"Environment OK" → "Your system looks good."
"Container image ready" → "Sandbox ready."
"OneCLI installed" → "OneCLI vault ready."
"Anthropic credential" → "Claude account"
"Mount allowlist in place" → "Access rules set."
"Service installed/running" → "NanoClaw is running."
"Wiring the terminal agent" → "Setting up your terminal chat…"
"Setup complete" → "You're ready! Enjoy NanoClaw."
Long-running steps get a one-sentence "why" that teaches a NanoClaw
differentiator while the user waits:
bootstrap → "NanoClaw is small and runs entirely on your machine.
Yours to modify."
container → "Your assistant lives in its own sandbox. It can only
see what you explicitly share."
onecli → "Your assistant never gets your API keys directly. The
vault adds them to approved requests as they leave the
sandbox."
OneCLI is now named explicitly and framed as "your agent's vault" in
the install step, the paste-auth save step, the subscription-auth
banner, and their associated failure hints.
Auth split (option b: explicit step name on fail): the auth-method
choice moves from the bash menu in register-claude-token.sh into a
clack select. Only the subscription path still breaks out to the
interactive TTY for `claude setup-token`; paste-based OAuth tokens and
API keys stay in clack via p.password() and register directly via
`onecli secrets create`. register-claude-token.sh is scoped down to
the subscription flow only.
nanoclaw.sh: dropped the "Phase 1 / Phase 2" labels. The wordmark and
subtitle now print bash-side so setup:auto skips repeating them and
the flow reads as one continuous sequence. Bootstrap label is
"Installing the basics" with a dim gutter-line "why" preamble. pnpm's
`> nanoclaw@X setup:auto` preamble is suppressed via --silent.
Em-dash pass on user-facing copy: every em-dash that functions as an
em-dash in a user-visible string is replaced with period, semicolon,
comma, or parens. Code comments and JSDoc are untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+44
-33
@@ -1,15 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# NanoClaw — scripted end-to-end install.
|
||||
# NanoClaw — end-to-end setup entry point.
|
||||
#
|
||||
# Phase 1: bootstrap (Node + pnpm + native module verify). Runs bash-side
|
||||
# since tsx isn't available until pnpm install completes.
|
||||
# Phase 2: setup:auto (all remaining steps under clack).
|
||||
# Runs two parts from the user's perspective as one continuous flow:
|
||||
# - bash-side: install the basics (Node + pnpm + native modules) under a
|
||||
# bash-rendered clack-alike spinner. Can't use setup/auto.ts here since
|
||||
# tsx isn't available until pnpm install completes.
|
||||
# - hand off to `pnpm run setup:auto`, which renders the rest with
|
||||
# @clack/prompts. The wordmark is printed once here so setup:auto can
|
||||
# skip it and the flow reads as a single sequence.
|
||||
#
|
||||
# Both phases obey the same three-level output contract (see
|
||||
# docs/setup-flow.md):
|
||||
# Obeys the three-level output contract (see docs/setup-flow.md):
|
||||
# 1. User-facing — concise status line with elapsed time
|
||||
# 2. Progression log — logs/setup.log (header + one entry per phase/step)
|
||||
# 2. Progression log — logs/setup.log (header + one entry per step)
|
||||
# 3. Raw per-step log — logs/setup-steps/NN-name.log (full verbatim output)
|
||||
#
|
||||
# Config via env — passed through unchanged:
|
||||
@@ -91,6 +94,19 @@ use_ansi() { [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; }
|
||||
dim() { use_ansi && printf '\033[2m%s\033[0m' "$1" || printf '%s' "$1"; }
|
||||
gray() { use_ansi && printf '\033[90m%s\033[0m' "$1" || printf '%s' "$1"; }
|
||||
red() { use_ansi && printf '\033[31m%s\033[0m' "$1" || printf '%s' "$1"; }
|
||||
bold() { use_ansi && printf '\033[1m%s\033[0m' "$1" || printf '%s' "$1"; }
|
||||
# brand cyan (≈ #2BB7CE) — truecolor when supported, 16-color cyan fallback.
|
||||
brand_bold() {
|
||||
if use_ansi; then
|
||||
if [ "${COLORTERM:-}" = "truecolor" ] || [ "${COLORTERM:-}" = "24bit" ]; then
|
||||
printf '\033[1;38;2;43;183;206m%s\033[0m' "$1"
|
||||
else
|
||||
printf '\033[1;36m%s\033[0m' "$1"
|
||||
fi
|
||||
else
|
||||
printf '%s' "$1"
|
||||
fi
|
||||
}
|
||||
clear_line() { use_ansi && printf '\r\033[2K' || printf '\n'; }
|
||||
|
||||
spinner_start() { printf '%s %s…' "$(gray '◒')" "$1"; }
|
||||
@@ -105,21 +121,20 @@ rm -f "$PROGRESS_LOG"
|
||||
mkdir -p "$STEPS_DIR" "$LOGS_DIR"
|
||||
write_header
|
||||
|
||||
cat <<'EOF'
|
||||
═══════════════════════════════════════════════════════════════
|
||||
NanoClaw scripted setup
|
||||
═══════════════════════════════════════════════════════════════
|
||||
# NanoClaw wordmark + subtitle — setup:auto will see NANOCLAW_BOOTSTRAPPED=1
|
||||
# and skip printing these again, so the flow stays visually continuous.
|
||||
printf '\n %s%s\n' "$(bold 'Nano')" "$(brand_bold 'Claw')"
|
||||
printf ' %s\n\n' "$(dim 'Setting up your personal AI assistant')"
|
||||
|
||||
Phase 1 · bootstrap
|
||||
|
||||
EOF
|
||||
|
||||
# ─── phase 1: bootstrap ─────────────────────────────────────────────────
|
||||
# ─── first step: install the basics (Node + pnpm + native modules) ─────
|
||||
|
||||
BOOTSTRAP_RAW="${STEPS_DIR}/01-bootstrap.log"
|
||||
BOOTSTRAP_LABEL="Bootstrapping Node, pnpm, native modules"
|
||||
BOOTSTRAP_LABEL="Installing the basics"
|
||||
BOOTSTRAP_START=$(date +%s)
|
||||
|
||||
# One-line "why" that teaches a differentiator while the user waits.
|
||||
printf '%s %s\n' "$(gray '│')" \
|
||||
"$(dim "NanoClaw is small and runs entirely on your machine. Yours to modify.")"
|
||||
spinner_start "$BOOTSTRAP_LABEL"
|
||||
|
||||
# Run in the background so we can tick elapsed time. Capture exit code via
|
||||
@@ -151,10 +166,10 @@ rm -f "$BOOTSTRAP_EXIT_FILE"
|
||||
BOOTSTRAP_DUR=$(( $(date +%s) - BOOTSTRAP_START ))
|
||||
|
||||
if [ "$BOOTSTRAP_RC" -eq 0 ]; then
|
||||
spinner_success "Bootstrap complete" "$BOOTSTRAP_DUR"
|
||||
spinner_success "Basics installed" "$BOOTSTRAP_DUR"
|
||||
write_bootstrap_entry success "$BOOTSTRAP_DUR" "$BOOTSTRAP_RAW"
|
||||
else
|
||||
spinner_failure "Bootstrap failed" "$BOOTSTRAP_DUR"
|
||||
spinner_failure "Couldn't install the basics" "$BOOTSTRAP_DUR"
|
||||
write_bootstrap_entry failed "$BOOTSTRAP_DUR" "$BOOTSTRAP_RAW"
|
||||
write_abort_entry bootstrap "exit-${BOOTSTRAP_RC}"
|
||||
|
||||
@@ -162,23 +177,19 @@ else
|
||||
echo "$(dim '── last 40 lines of ')$(dim "$BOOTSTRAP_RAW")$(dim ' ──')"
|
||||
tail -40 "$BOOTSTRAP_RAW"
|
||||
echo
|
||||
echo "Full raw log: $BOOTSTRAP_RAW"
|
||||
echo "Progression: $PROGRESS_LOG"
|
||||
echo "$(dim "Full raw log: $BOOTSTRAP_RAW")"
|
||||
echo "$(dim "Progression: $PROGRESS_LOG")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
cat <<'EOF'
|
||||
Phase 2 · setup:auto
|
||||
# ─── hand off to setup:auto ────────────────────────────────────────────
|
||||
|
||||
EOF
|
||||
|
||||
# ─── phase 2: clack driver ──────────────────────────────────────────────
|
||||
|
||||
# NANOCLAW_BOOTSTRAPPED=1 tells setup/auto.ts that the progression log has
|
||||
# already been initialized (header + bootstrap entry), so it should append
|
||||
# rather than wipe.
|
||||
# NANOCLAW_BOOTSTRAPPED=1 tells setup/auto.ts to skip the wordmark (we
|
||||
# already printed it) and to append to the progression log rather than
|
||||
# wipe it.
|
||||
export NANOCLAW_BOOTSTRAPPED=1
|
||||
|
||||
# exec so signals (Ctrl-C) propagate directly to the child.
|
||||
exec pnpm run setup:auto
|
||||
# --silent suppresses pnpm's `> nanoclaw@1.2.52 setup:auto / > tsx setup/auto.ts`
|
||||
# preamble so the flow continues visually from "Basics installed" straight
|
||||
# into setup:auto's spinner. exec so signals (Ctrl-C) propagate directly.
|
||||
exec pnpm --silent run setup:auto
|
||||
|
||||
+205
-81
@@ -27,7 +27,7 @@ import k from 'kleur';
|
||||
|
||||
import { runTelegramChannel } from './channels/telegram.js';
|
||||
import * as setupLog from './logs.js';
|
||||
import { ensureAnswer, fail, runQuietStep } from './lib/runner.js';
|
||||
import { ensureAnswer, fail, runQuietChild, runQuietStep } from './lib/runner.js';
|
||||
import { brandBold, brandChip } from './lib/theme.js';
|
||||
|
||||
const CLI_AGENT_NAME = 'Terminal Agent';
|
||||
@@ -46,121 +46,116 @@ async function main(): Promise<void> {
|
||||
|
||||
if (!skip.has('environment')) {
|
||||
const res = await runQuietStep('environment', {
|
||||
running: 'Checking environment…',
|
||||
done: 'Environment OK.',
|
||||
running: 'Checking your system…',
|
||||
done: 'Your system looks good.',
|
||||
});
|
||||
if (!res.ok) fail('environment', 'Environment check failed.');
|
||||
if (!res.ok) {
|
||||
fail(
|
||||
'environment',
|
||||
"Your system doesn't look quite right.",
|
||||
'See logs/setup-steps/ for details, then retry.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip.has('container')) {
|
||||
p.log.message(
|
||||
k.dim(
|
||||
'Your assistant lives in its own sandbox. It can only see what you explicitly share.',
|
||||
),
|
||||
);
|
||||
const res = await runQuietStep('container', {
|
||||
running: 'Building the agent container image…',
|
||||
done: 'Container image ready.',
|
||||
failed: 'Container build failed.',
|
||||
running: 'Preparing the sandbox your assistant runs in…',
|
||||
done: 'Sandbox ready.',
|
||||
failed: "Couldn't prepare the sandbox.",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = res.terminal?.fields.ERROR;
|
||||
if (err === 'runtime_not_available') {
|
||||
fail(
|
||||
'container',
|
||||
'Docker is not available and could not be started automatically.',
|
||||
'Install Docker Desktop or start it manually, then retry.',
|
||||
"Docker isn't available.",
|
||||
'Install Docker Desktop (or start it if already installed), then retry.',
|
||||
);
|
||||
}
|
||||
if (err === 'docker_group_not_active') {
|
||||
fail(
|
||||
'container',
|
||||
'Docker was just installed but your shell is not yet in the `docker` group.',
|
||||
"Docker was just installed but your shell doesn't know yet.",
|
||||
'Log out and back in (or run `newgrp docker` in a new shell), then retry.',
|
||||
);
|
||||
}
|
||||
fail(
|
||||
'container',
|
||||
'Container build/test failed.',
|
||||
'For stale cache: `docker builder prune -f`, then retry `pnpm run setup:auto`.',
|
||||
"Couldn't build the sandbox.",
|
||||
'If Docker has a stale cache, try: `docker builder prune -f`, then retry.',
|
||||
);
|
||||
}
|
||||
maybeReexecUnderSg();
|
||||
}
|
||||
|
||||
if (!skip.has('onecli')) {
|
||||
p.log.message(
|
||||
k.dim(
|
||||
'Your assistant never gets your API keys directly. The vault adds them to approved requests as they leave the sandbox.',
|
||||
),
|
||||
);
|
||||
const res = await runQuietStep('onecli', {
|
||||
running: 'Installing OneCLI credential vault…',
|
||||
done: 'OneCLI installed.',
|
||||
running: "Setting up OneCLI, your agent's vault…",
|
||||
done: 'OneCLI vault ready.',
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = res.terminal?.fields.ERROR;
|
||||
if (err === 'onecli_not_on_path_after_install') {
|
||||
fail(
|
||||
'onecli',
|
||||
'OneCLI installed but not on PATH.',
|
||||
'OneCLI was installed but your shell needs to refresh to see it.',
|
||||
'Open a new shell or run `export PATH="$HOME/.local/bin:$PATH"`, then retry.',
|
||||
);
|
||||
}
|
||||
fail(
|
||||
'onecli',
|
||||
`OneCLI install failed (${err ?? 'unknown'}).`,
|
||||
'Check that curl + a writable ~/.local/bin are available, then retry.',
|
||||
`Couldn't set up OneCLI (${err ?? 'unknown error'}).`,
|
||||
'Make sure curl is installed and ~/.local/bin is writable, then retry.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip.has('auth')) {
|
||||
if (anthropicSecretExists()) {
|
||||
p.log.success('OneCLI already has an Anthropic secret — skipping.');
|
||||
setupLog.step('auth', 'skipped', 0, { REASON: 'secret-already-present' });
|
||||
} else {
|
||||
p.log.step('Registering your Anthropic credential…');
|
||||
console.log(
|
||||
k.dim(' (browser sign-in or paste a token/key — this part is interactive)'),
|
||||
);
|
||||
console.log();
|
||||
const start = Date.now();
|
||||
const code = await runInheritScript('bash', ['setup/register-claude-token.sh']);
|
||||
const durationMs = Date.now() - start;
|
||||
console.log();
|
||||
if (code !== 0) {
|
||||
setupLog.step('auth', 'failed', durationMs, { EXIT_CODE: code });
|
||||
fail(
|
||||
'auth',
|
||||
'Anthropic credential registration failed or was aborted.',
|
||||
'Re-run `bash setup/register-claude-token.sh` or handle via `/setup` §4.',
|
||||
);
|
||||
}
|
||||
setupLog.step('auth', 'interactive', durationMs, {
|
||||
METHOD: 'register-claude-token.sh',
|
||||
});
|
||||
p.log.success('Anthropic credential registered with OneCLI.');
|
||||
}
|
||||
await runAuthStep();
|
||||
}
|
||||
|
||||
if (!skip.has('mounts')) {
|
||||
const res = await runQuietStep(
|
||||
'mounts',
|
||||
{
|
||||
running: 'Writing mount allowlist…',
|
||||
done: 'Mount allowlist in place.',
|
||||
skipped: 'Mount allowlist already configured.',
|
||||
running: "Setting your assistant's access rules…",
|
||||
done: 'Access rules set.',
|
||||
skipped: 'Access rules already set.',
|
||||
},
|
||||
['--empty'],
|
||||
);
|
||||
if (!res.ok) fail('mounts', 'Mount allowlist step failed.');
|
||||
if (!res.ok) {
|
||||
fail('mounts', "Couldn't write access rules.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip.has('service')) {
|
||||
const res = await runQuietStep('service', {
|
||||
running: 'Installing the background service…',
|
||||
done: 'Service installed and running.',
|
||||
running: 'Starting NanoClaw in the background…',
|
||||
done: 'NanoClaw is running.',
|
||||
});
|
||||
if (!res.ok) {
|
||||
fail(
|
||||
'service',
|
||||
'Service install failed.',
|
||||
'Check logs/nanoclaw.error.log, or run `/setup` to iterate interactively.',
|
||||
"Couldn't start NanoClaw.",
|
||||
'See logs/nanoclaw.error.log for details.',
|
||||
);
|
||||
}
|
||||
if (res.terminal?.fields.DOCKER_GROUP_STALE === 'true') {
|
||||
p.log.warn('Docker group stale in systemd session.');
|
||||
p.log.warn(
|
||||
"NanoClaw's permissions need a tweak before it can reach Docker.",
|
||||
);
|
||||
p.log.message(
|
||||
k.dim(
|
||||
' sudo setfacl -m u:$(whoami):rw /var/run/docker.sock\n' +
|
||||
@@ -182,16 +177,16 @@ async function main(): Promise<void> {
|
||||
const res = await runQuietStep(
|
||||
'cli-agent',
|
||||
{
|
||||
running: 'Wiring the terminal agent…',
|
||||
done: 'Terminal agent wired (try `pnpm run chat hi`).',
|
||||
running: 'Setting up your terminal chat…',
|
||||
done: 'Terminal chat ready. Try `pnpm run chat hi`.',
|
||||
},
|
||||
['--display-name', displayName!, '--agent-name', CLI_AGENT_NAME],
|
||||
);
|
||||
if (!res.ok) {
|
||||
fail(
|
||||
'cli-agent',
|
||||
'CLI agent wiring failed.',
|
||||
`Re-run \`pnpm exec tsx scripts/init-cli-agent.ts --display-name "${displayName!}" --agent-name "${CLI_AGENT_NAME}"\` to fix.`,
|
||||
"Couldn't set up the terminal chat.",
|
||||
`You can retry later with \`pnpm exec tsx scripts/init-cli-agent.ts --display-name "${displayName!}" --agent-name "${CLI_AGENT_NAME}"\`.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -201,47 +196,165 @@ async function main(): Promise<void> {
|
||||
if (choice === 'telegram') {
|
||||
await runTelegramChannel(displayName!);
|
||||
} else {
|
||||
p.log.info('No messaging channel wired — you can add one later with `/add-<channel>`.');
|
||||
p.log.info(
|
||||
"No messaging app for now. You can add one later (like Telegram, Slack, or Discord).",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip.has('verify')) {
|
||||
const res = await runQuietStep('verify', {
|
||||
running: 'Verifying the install…',
|
||||
done: 'Install verified.',
|
||||
failed: 'Verification found issues.',
|
||||
running: 'Making sure everything works together…',
|
||||
done: "Everything's connected.",
|
||||
failed: 'A few things still need your attention.',
|
||||
});
|
||||
if (!res.ok) {
|
||||
const notes: string[] = [];
|
||||
if (res.terminal?.fields.CREDENTIALS !== 'configured') {
|
||||
notes.push('• Anthropic secret not detected — re-run `bash setup/register-claude-token.sh`.');
|
||||
notes.push('• Your Claude account isn\'t connected. Re-run setup and try again.');
|
||||
}
|
||||
const agentPing = res.terminal?.fields.AGENT_PING;
|
||||
if (agentPing && agentPing !== 'ok' && agentPing !== 'skipped') {
|
||||
notes.push(
|
||||
`• CLI agent did not reply (status: ${agentPing}). ` +
|
||||
'Check `logs/nanoclaw.log` and `groups/*/logs/container-*.log`, then try `pnpm run chat hi`.',
|
||||
"• Your assistant didn't reply to a test message. " +
|
||||
'Check `logs/nanoclaw.log` for clues, then try `pnpm run chat hi`.',
|
||||
);
|
||||
}
|
||||
if (!res.terminal?.fields.CONFIGURED_CHANNELS) {
|
||||
notes.push('• Optional: add a messaging channel — `/add-discord`, `/add-slack`, `/add-telegram`, …');
|
||||
notes.push('• Want to chat from your phone? Add a messaging app with `/add-telegram`, `/add-slack`, or `/add-discord`.');
|
||||
}
|
||||
if (notes.length > 0) {
|
||||
p.note(notes.join('\n'), 'What’s left');
|
||||
p.note(notes.join('\n'), "What's left");
|
||||
}
|
||||
p.outro(k.yellow('Scripted steps done — some pieces still need you.'));
|
||||
p.outro(k.yellow('Almost there. A few things still need your attention.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const nextSteps = [
|
||||
`${k.cyan('Chat from the CLI:')} pnpm run chat hi`,
|
||||
`${k.cyan('Tail host logs:')} tail -f logs/nanoclaw.log`,
|
||||
`${k.cyan('Open Claude Code:')} claude`,
|
||||
].join('\n');
|
||||
p.note(nextSteps, 'Next steps');
|
||||
const rows: [string, string][] = [
|
||||
['Chat in the terminal:', 'pnpm run chat hi'],
|
||||
["See what's happening:", 'tail -f logs/nanoclaw.log'],
|
||||
['Open Claude Code:', 'claude'],
|
||||
];
|
||||
const labelWidth = Math.max(...rows.map(([l]) => l.length));
|
||||
const nextSteps = rows
|
||||
.map(([l, c]) => `${k.cyan(l.padEnd(labelWidth))} ${c}`)
|
||||
.join('\n');
|
||||
p.note(nextSteps, 'Try these');
|
||||
setupLog.complete(Date.now() - RUN_START);
|
||||
p.outro(k.green('Setup complete.'));
|
||||
p.outro(k.green("You're ready! Enjoy NanoClaw."));
|
||||
}
|
||||
|
||||
// ─── auth step (select → branch) ────────────────────────────────────────
|
||||
|
||||
async function runAuthStep(): Promise<void> {
|
||||
if (anthropicSecretExists()) {
|
||||
p.log.success('Your Claude account is already connected.');
|
||||
setupLog.step('auth', 'skipped', 0, { REASON: 'secret-already-present' });
|
||||
return;
|
||||
}
|
||||
|
||||
const method = ensureAnswer(
|
||||
await p.select({
|
||||
message: 'How would you like to connect to Claude?',
|
||||
options: [
|
||||
{
|
||||
value: 'subscription',
|
||||
label: 'Sign in with my Claude subscription',
|
||||
hint: 'recommended if you have Pro or Max',
|
||||
},
|
||||
{
|
||||
value: 'oauth',
|
||||
label: 'Paste an OAuth token I already have',
|
||||
hint: 'sk-ant-oat…',
|
||||
},
|
||||
{
|
||||
value: 'api',
|
||||
label: 'Paste an Anthropic API key',
|
||||
hint: 'pay-per-use via console.anthropic.com',
|
||||
},
|
||||
],
|
||||
}),
|
||||
) as 'subscription' | 'oauth' | 'api';
|
||||
setupLog.userInput('auth_method', method);
|
||||
|
||||
if (method === 'subscription') {
|
||||
await runSubscriptionAuth();
|
||||
} else {
|
||||
await runPasteAuth(method);
|
||||
}
|
||||
}
|
||||
|
||||
async function runSubscriptionAuth(): Promise<void> {
|
||||
p.log.step("Opening the Claude sign-in flow…");
|
||||
console.log(
|
||||
k.dim(' (a browser will open for sign-in; this part is interactive)'),
|
||||
);
|
||||
console.log();
|
||||
const start = Date.now();
|
||||
const code = await runInheritScript('bash', [
|
||||
'setup/register-claude-token.sh',
|
||||
]);
|
||||
const durationMs = Date.now() - start;
|
||||
console.log();
|
||||
if (code !== 0) {
|
||||
setupLog.step('auth', 'failed', durationMs, {
|
||||
EXIT_CODE: code,
|
||||
METHOD: 'subscription',
|
||||
});
|
||||
fail(
|
||||
'auth',
|
||||
"Couldn't complete the Claude sign-in.",
|
||||
'Re-run setup and try again, or choose a paste option instead.',
|
||||
);
|
||||
}
|
||||
setupLog.step('auth', 'interactive', durationMs, { METHOD: 'subscription' });
|
||||
p.log.success('Claude account connected.');
|
||||
}
|
||||
|
||||
async function runPasteAuth(method: 'oauth' | 'api'): Promise<void> {
|
||||
const label = method === 'oauth' ? 'OAuth token' : 'API key';
|
||||
const prefix = method === 'oauth' ? 'sk-ant-oat' : 'sk-ant-api';
|
||||
|
||||
const answer = ensureAnswer(
|
||||
await p.password({
|
||||
message: `Paste your ${label}`,
|
||||
validate: (v) => {
|
||||
if (!v || !v.trim()) return 'Required';
|
||||
if (!v.trim().startsWith(prefix)) {
|
||||
return `Should start with ${prefix}…`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
);
|
||||
const token = (answer as string).trim();
|
||||
|
||||
const res = await runQuietChild(
|
||||
'auth',
|
||||
'onecli',
|
||||
[
|
||||
'secrets', 'create',
|
||||
'--name', 'Anthropic',
|
||||
'--type', 'anthropic',
|
||||
'--value', token,
|
||||
'--host-pattern', 'api.anthropic.com',
|
||||
],
|
||||
{
|
||||
running: `Saving your ${label} to your OneCLI vault…`,
|
||||
done: 'Claude account connected.',
|
||||
},
|
||||
{
|
||||
extraFields: { METHOD: method },
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
fail(
|
||||
'auth',
|
||||
`Couldn't save your ${label} to the vault.`,
|
||||
'Make sure OneCLI is running (`onecli version`), then retry.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── prompts owned by the sequencer ────────────────────────────────────
|
||||
@@ -249,7 +362,7 @@ async function main(): Promise<void> {
|
||||
async function askDisplayName(fallback: string): Promise<string> {
|
||||
const answer = ensureAnswer(
|
||||
await p.text({
|
||||
message: 'What should your agents call you?',
|
||||
message: 'What should your assistant call you?',
|
||||
placeholder: fallback,
|
||||
defaultValue: fallback,
|
||||
}),
|
||||
@@ -262,10 +375,10 @@ async function askDisplayName(fallback: string): Promise<string> {
|
||||
async function askChannelChoice(): Promise<'telegram' | 'skip'> {
|
||||
const choice = ensureAnswer(
|
||||
await p.select({
|
||||
message: 'Connect a messaging app so you can chat from your phone?',
|
||||
message: 'Want to chat with your assistant from your phone?',
|
||||
options: [
|
||||
{ value: 'telegram', label: 'Telegram', hint: 'recommended' },
|
||||
{ value: 'skip', label: 'Skip — use the CLI only' },
|
||||
{ value: 'telegram', label: 'Yes, connect Telegram', hint: 'recommended' },
|
||||
{ value: 'skip', label: 'Skip for now', hint: "I'll just use the terminal" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -311,7 +424,7 @@ function maybeReexecUnderSg(): void {
|
||||
if (!/permission denied/i.test(err)) return;
|
||||
if (spawnSync('which', ['sg'], { stdio: 'ignore' }).status !== 0) return;
|
||||
|
||||
p.log.warn('Docker socket not accessible in current group — re-executing under `sg docker`.');
|
||||
p.log.warn('Docker socket not accessible in current group. Re-executing under `sg docker`.');
|
||||
const res = spawnSync('sg', ['docker', '-c', 'pnpm run setup:auto'], {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, NANOCLAW_REEXEC_SG: '1' },
|
||||
@@ -323,17 +436,28 @@ function maybeReexecUnderSg(): void {
|
||||
|
||||
function printIntro(): void {
|
||||
const isReexec = process.env.NANOCLAW_REEXEC_SG === '1';
|
||||
const isBootstrapped = process.env.NANOCLAW_BOOTSTRAPPED === '1';
|
||||
const wordmark = `${k.bold('Nano')}${brandBold('Claw')}`;
|
||||
|
||||
if (isReexec) {
|
||||
p.intro(`${brandChip(' setup:auto ')} ${wordmark} ${k.dim('· resuming under docker group')}`);
|
||||
p.intro(
|
||||
`${brandChip(' Welcome ')} ${wordmark} ${k.dim('· picking up where we left off')}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// When we were called via nanoclaw.sh, the wordmark + subtitle were
|
||||
// already printed in bash. Just open the clack gutter with a short,
|
||||
// neutral intro so the flow continues without duplication.
|
||||
if (isBootstrapped) {
|
||||
p.intro(k.dim("Let's get you set up."));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(` ${wordmark}`);
|
||||
console.log(` ${k.dim('end-to-end scripted setup of your personal assistant')}`);
|
||||
p.intro(`${brandChip(' setup:auto ')}`);
|
||||
console.log(` ${k.dim('Setting up your personal AI assistant')}`);
|
||||
p.intro(k.dim("Let's get you set up."));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+40
-38
@@ -43,8 +43,8 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
|
||||
'bash',
|
||||
['setup/add-telegram.sh'],
|
||||
{
|
||||
running: `Installing Telegram adapter and wiring @${botUsername}…`,
|
||||
done: 'Telegram adapter ready.',
|
||||
running: `Connecting Telegram to @${botUsername}…`,
|
||||
done: 'Telegram connected.',
|
||||
},
|
||||
{
|
||||
env: { TELEGRAM_BOT_TOKEN: token },
|
||||
@@ -54,8 +54,8 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
|
||||
if (!install.ok) {
|
||||
fail(
|
||||
'telegram-install',
|
||||
'Telegram install failed.',
|
||||
'Check the raw log under logs/setup-steps/, then retry `pnpm run setup:auto`.',
|
||||
"Couldn't connect Telegram.",
|
||||
'See logs/setup-steps/ for details, then retry setup.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
|
||||
if (!pair.ok) {
|
||||
fail(
|
||||
'pair-telegram',
|
||||
'Telegram pairing failed.',
|
||||
'Re-run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent main`.',
|
||||
"Couldn't pair with Telegram.",
|
||||
'Re-run setup to try again.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
|
||||
if (!platformId || !pairedUserId) {
|
||||
fail(
|
||||
'pair-telegram',
|
||||
'pair-telegram succeeded but did not return PLATFORM_ID and PAIRED_USER_ID.',
|
||||
'Re-run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent main` and capture the success block.',
|
||||
'Pairing completed but came back incomplete.',
|
||||
'Re-run setup to try again.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,8 +92,8 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
|
||||
'--agent-name', agentName,
|
||||
],
|
||||
{
|
||||
running: `Wiring ${agentName} to your Telegram chat…`,
|
||||
done: `${agentName} is wired — welcome DM incoming.`,
|
||||
running: `Connecting ${agentName} to your Telegram chat…`,
|
||||
done: `${agentName} is ready. Check Telegram for a welcome message.`,
|
||||
},
|
||||
{
|
||||
extraFields: { CHANNEL: 'telegram', AGENT_NAME: agentName, PLATFORM_ID: platformId },
|
||||
@@ -102,8 +102,8 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
|
||||
if (!init.ok) {
|
||||
fail(
|
||||
'init-first-agent',
|
||||
'Wiring the Telegram agent failed.',
|
||||
`Re-run \`pnpm exec tsx scripts/init-first-agent.ts --channel telegram --user-id "${pairedUserId}" --platform-id "${platformId}" --display-name "${displayName}" --agent-name "${agentName}"\`.`,
|
||||
`Couldn't finish connecting ${agentName}.`,
|
||||
'You can retry later with `/manage-channels`.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -111,24 +111,26 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
|
||||
async function collectTelegramToken(): Promise<string> {
|
||||
p.note(
|
||||
[
|
||||
'1. Open Telegram and message @BotFather',
|
||||
'2. Send: /newbot',
|
||||
'3. Follow the prompts (name + username ending in "bot")',
|
||||
'4. Copy the token it gives you (format: <digits>:<chars>)',
|
||||
"Your assistant talks to you through a Telegram bot you create.",
|
||||
"Here's how:",
|
||||
'',
|
||||
k.dim('Optional, but recommended for groups:'),
|
||||
k.dim(' @BotFather → /mybots → Bot Settings → Group Privacy → OFF'),
|
||||
' 1. Open Telegram and message @BotFather',
|
||||
' 2. Send /newbot and follow the prompts',
|
||||
' 3. Copy the token it gives you (it looks like <digits>:<chars>)',
|
||||
'',
|
||||
k.dim('Planning to add your assistant to group chats? In @BotFather:'),
|
||||
k.dim(' /mybots → your bot → Bot Settings → Group Privacy → OFF'),
|
||||
].join('\n'),
|
||||
'Create a Telegram bot',
|
||||
'Set up your Telegram bot',
|
||||
);
|
||||
|
||||
const answer = ensureAnswer(
|
||||
await p.password({
|
||||
message: 'Paste your bot token',
|
||||
validate: (v) => {
|
||||
if (!v || !v.trim()) return 'Token is required';
|
||||
if (!v || !v.trim()) return "Token is required";
|
||||
if (!/^[0-9]+:[A-Za-z0-9_-]{35,}$/.test(v.trim())) {
|
||||
return 'Format looks wrong — expected <digits>:<chars>';
|
||||
return "That doesn't look right. It should be <digits>:<chars>";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
@@ -145,7 +147,7 @@ async function collectTelegramToken(): Promise<string> {
|
||||
async function validateTelegramToken(token: string): Promise<string> {
|
||||
const s = p.spinner();
|
||||
const start = Date.now();
|
||||
s.start('Validating token with Telegram…');
|
||||
s.start('Checking your bot token…');
|
||||
try {
|
||||
const res = await fetch(`https://api.telegram.org/bot${token}/getMe`);
|
||||
const data = (await res.json()) as {
|
||||
@@ -156,7 +158,7 @@ async function validateTelegramToken(token: string): Promise<string> {
|
||||
const elapsedS = Math.round((Date.now() - start) / 1000);
|
||||
if (data.ok && data.result?.username) {
|
||||
const username = data.result.username;
|
||||
s.stop(`Bot is @${username}. ${k.dim(`(${elapsedS}s)`)}`);
|
||||
s.stop(`Found your bot: @${username}. ${k.dim(`(${elapsedS}s)`)}`);
|
||||
setupLog.step('telegram-validate', 'success', Date.now() - start, {
|
||||
BOT_USERNAME: username,
|
||||
BOT_ID: data.result.id ?? '',
|
||||
@@ -164,26 +166,26 @@ async function validateTelegramToken(token: string): Promise<string> {
|
||||
return username;
|
||||
}
|
||||
const reason = data.description ?? 'token rejected by Telegram';
|
||||
s.stop(`Telegram rejected the token: ${reason}`, 1);
|
||||
s.stop(`Telegram didn't accept that token: ${reason}`, 1);
|
||||
setupLog.step('telegram-validate', 'failed', Date.now() - start, {
|
||||
ERROR: reason,
|
||||
});
|
||||
fail(
|
||||
'telegram-validate',
|
||||
'Telegram rejected the token.',
|
||||
'Double-check the token (copy it again from @BotFather) and retry.',
|
||||
"Telegram didn't accept that token.",
|
||||
'Copy the token again from @BotFather and try setup once more.',
|
||||
);
|
||||
} catch (err) {
|
||||
const elapsedS = Math.round((Date.now() - start) / 1000);
|
||||
s.stop(`Could not reach Telegram. ${k.dim(`(${elapsedS}s)`)}`, 1);
|
||||
s.stop(`Couldn't reach Telegram. ${k.dim(`(${elapsedS}s)`)}`, 1);
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setupLog.step('telegram-validate', 'failed', Date.now() - start, {
|
||||
ERROR: message,
|
||||
});
|
||||
fail(
|
||||
'telegram-validate',
|
||||
'Telegram API unreachable.',
|
||||
'Check your network connection and retry.',
|
||||
"Couldn't reach Telegram.",
|
||||
'Check your internet connection and retry setup.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -194,7 +196,7 @@ async function runPairTelegram(): Promise<
|
||||
const rawLog = setupLog.stepRawLog('pair-telegram');
|
||||
const start = Date.now();
|
||||
const s = p.spinner();
|
||||
s.start('Creating pairing code…');
|
||||
s.start('Generating a secret code for your bot…');
|
||||
let spinnerActive = true;
|
||||
|
||||
const stopSpinner = (msg: string, code?: number) => {
|
||||
@@ -211,15 +213,15 @@ async function runPairTelegram(): Promise<
|
||||
if (block.type === 'PAIR_TELEGRAM_CODE') {
|
||||
const reason = block.fields.REASON ?? 'initial';
|
||||
if (reason === 'initial') {
|
||||
stopSpinner('Pairing code ready.');
|
||||
stopSpinner('Your secret code is ready.');
|
||||
} else {
|
||||
stopSpinner('Previous code invalidated. New code below.');
|
||||
stopSpinner("Old code expired. Here's a fresh one.");
|
||||
}
|
||||
p.note(formatCodeCard(block.fields.CODE ?? '????'), 'Pairing code');
|
||||
s.start('Waiting for the code from Telegram…');
|
||||
p.note(formatCodeCard(block.fields.CODE ?? '????'), 'Secret code');
|
||||
s.start('Waiting for you to send the code from Telegram…');
|
||||
spinnerActive = true;
|
||||
} else if (block.type === 'PAIR_TELEGRAM_ATTEMPT') {
|
||||
stopSpinner(`Received "${block.fields.CANDIDATE ?? '?'}" — doesn't match.`);
|
||||
stopSpinner(`Got "${block.fields.CANDIDATE ?? '?'}", not a match.`);
|
||||
s.start('Waiting for the correct code…');
|
||||
spinnerActive = true;
|
||||
} else if (block.type === 'PAIR_TELEGRAM') {
|
||||
@@ -238,7 +240,7 @@ async function runPairTelegram(): Promise<
|
||||
// sure we don't leave the spinner running.
|
||||
if (spinnerActive) {
|
||||
stopSpinner(
|
||||
result.ok ? 'Done.' : 'Pairing exited unexpectedly.',
|
||||
result.ok ? 'Done.' : 'Pairing ended unexpectedly.',
|
||||
result.ok ? 0 : 1,
|
||||
);
|
||||
if (!result.ok) dumpTranscriptOnFailure(result.transcript);
|
||||
@@ -254,7 +256,7 @@ function formatCodeCard(code: string): string {
|
||||
'',
|
||||
` ${brandBold(spaced)}`,
|
||||
'',
|
||||
k.dim(' Send these digits from Telegram to your bot.'),
|
||||
k.dim(' Send this code to your bot from Telegram.'),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -266,7 +268,7 @@ async function resolveAgentName(): Promise<string> {
|
||||
}
|
||||
const answer = ensureAnswer(
|
||||
await p.text({
|
||||
message: 'What should your messaging agent be called?',
|
||||
message: 'What should your assistant be called?',
|
||||
placeholder: DEFAULT_AGENT_NAME,
|
||||
defaultValue: DEFAULT_AGENT_NAME,
|
||||
}),
|
||||
|
||||
@@ -1,128 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Prefer bash 4+ (for `read -e -i` readline preload). macOS ships 3.2 in
|
||||
# /bin/bash, but Homebrew users usually have 5.x first on PATH. The readline
|
||||
# preload is optional — on 3.x we fall back to a plain confirmation prompt.
|
||||
|
||||
# Register an Anthropic credential with OneCLI. Three paths:
|
||||
# 1) Claude subscription — run `claude setup-token` (browser sign-in)
|
||||
# and capture the resulting OAuth token.
|
||||
# 2) Paste an existing sk-ant-oat… OAuth token you already have.
|
||||
# 3) Paste an Anthropic API key (sk-ant-api…).
|
||||
# Register a Claude subscription OAuth token with OneCLI — the *only* auth
|
||||
# path that needs a TTY break in the flow. Paste-based paths (existing
|
||||
# OAuth token / API key) are handled in-process by setup/auto.ts using
|
||||
# clack prompts, then onecli secrets create is invoked directly from TS.
|
||||
#
|
||||
# Flow:
|
||||
# 1. Run `claude setup-token` under a PTY (via script(1)) so the browser
|
||||
# OAuth dance works and its token is captured into a tempfile.
|
||||
# 2. Regex the sk-ant-oat…AA token out of the ANSI-stripped capture.
|
||||
# 3. Register it with OneCLI.
|
||||
#
|
||||
# Env overrides:
|
||||
# SECRET_NAME OneCLI secret name (default: Anthropic)
|
||||
# HOST_PATTERN OneCLI host pattern (default: api.anthropic.com)
|
||||
|
||||
# Prefer bash 4+ (for `read -e -i` readline preload). macOS ships 3.2 in
|
||||
# /bin/bash, but Homebrew users usually have 5.x first on PATH. The
|
||||
# readline preload is optional — on 3.x we fall back to a plain prompt.
|
||||
|
||||
SECRET_NAME="${SECRET_NAME:-Anthropic}"
|
||||
HOST_PATTERN="${HOST_PATTERN:-api.anthropic.com}"
|
||||
|
||||
command -v onecli >/dev/null \
|
||||
|| { echo "onecli not found. Install it first (see /setup §4)." >&2; exit 1; }
|
||||
command -v claude >/dev/null \
|
||||
|| { echo "claude CLI not found. Install from https://claude.ai/download" >&2; exit 1; }
|
||||
command -v script >/dev/null \
|
||||
|| { echo "script(1) is required for PTY capture." >&2; exit 1; }
|
||||
|
||||
TOKEN=""
|
||||
tmpfile=$(mktemp -t claude-setup-token.XXXXXX)
|
||||
trap 'rm -f "$tmpfile"' EXIT
|
||||
|
||||
capture_via_claude_setup_token() {
|
||||
command -v claude >/dev/null \
|
||||
|| { echo "claude CLI not found. Install from https://claude.ai/download" >&2; exit 1; }
|
||||
command -v script >/dev/null \
|
||||
|| { echo "script(1) is required for PTY capture." >&2; exit 1; }
|
||||
cat <<'EOF'
|
||||
A browser window will open for you to sign in with your Claude account.
|
||||
When you finish, we'll save the token to your OneCLI vault automatically.
|
||||
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp -t claude-setup-token.XXXXXX)
|
||||
trap 'rm -f "$tmpfile"' RETURN
|
||||
|
||||
cat <<'EOF'
|
||||
A browser window will open for sign-in. Token is captured automatically.
|
||||
Press Enter to run, or edit the command first.
|
||||
Press Enter to continue, or edit the command first.
|
||||
|
||||
EOF
|
||||
|
||||
local cmd="claude setup-token"
|
||||
if [[ ${BASH_VERSINFO[0]:-0} -ge 4 ]]; then
|
||||
# bash 4+: pre-fill the readline buffer so Enter literally submits.
|
||||
read -r -e -i "$cmd" -p "$ " cmd </dev/tty
|
||||
else
|
||||
# bash 3.x (macOS default /bin/bash): no readline preload. Fall back.
|
||||
echo "$ $cmd"
|
||||
read -r -p "Press Enter to run, Ctrl-C to abort. " _ </dev/tty
|
||||
fi
|
||||
cmd="claude setup-token"
|
||||
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
||||
# bash 4+: pre-fill the readline buffer so Enter literally submits.
|
||||
read -r -e -i "$cmd" -p "$ " cmd </dev/tty
|
||||
else
|
||||
# bash 3.x (macOS default /bin/bash): no readline preload. Fall back.
|
||||
echo "$ $cmd"
|
||||
read -r -p "Press Enter to run, Ctrl-C to abort. " _ </dev/tty
|
||||
fi
|
||||
|
||||
# `script` arg order differs between BSD (macOS) and util-linux.
|
||||
if script --version 2>/dev/null | grep -q util-linux; then
|
||||
script -q -c "$cmd" "$tmpfile"
|
||||
else
|
||||
# BSD script: command is argv after the file, so let it word-split.
|
||||
# shellcheck disable=SC2086
|
||||
script -q "$tmpfile" $cmd
|
||||
fi
|
||||
# `script` arg order differs between BSD (macOS) and util-linux.
|
||||
if script --version 2>/dev/null | grep -q util-linux; then
|
||||
script -q -c "$cmd" "$tmpfile"
|
||||
else
|
||||
# BSD script: command is argv after the file, so let it word-split.
|
||||
# shellcheck disable=SC2086
|
||||
script -q "$tmpfile" $cmd
|
||||
fi
|
||||
|
||||
# Strip ANSI codes + newlines (TTY wraps the token mid-string), then match
|
||||
# the sk-ant-oat…AA token. perl because BSD grep caps {n,m} at 255.
|
||||
TOKEN=$(sed $'s/\x1b\\[[0-9;]*[a-zA-Z]//g' "$tmpfile" \
|
||||
| tr -d '\n\r' \
|
||||
| perl -ne 'print "$1\n" while /(sk-ant-oat[A-Za-z0-9_-]{80,500}AA)/g' \
|
||||
| tail -1 || true)
|
||||
# Strip ANSI codes + newlines (TTY wraps the token mid-string), then match
|
||||
# the sk-ant-oat…AA token. perl because BSD grep caps {n,m} at 255.
|
||||
token=$(sed $'s/\x1b\\[[0-9;]*[a-zA-Z]//g' "$tmpfile" \
|
||||
| tr -d '\n\r' \
|
||||
| perl -ne 'print "$1\n" while /(sk-ant-oat[A-Za-z0-9_-]{80,500}AA)/g' \
|
||||
| tail -1 || true)
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
local keep
|
||||
keep=$(mktemp -t claude-setup-token-log.XXXXXX)
|
||||
cp "$tmpfile" "$keep"
|
||||
echo >&2
|
||||
echo "No sk-ant-oat…AA token found. Raw log: $keep" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_for_pasted() {
|
||||
local prefix="$1" # "oat" or "api"
|
||||
local value
|
||||
echo
|
||||
echo "Paste your sk-ant-${prefix}… credential and press Enter."
|
||||
echo "Nothing will appear on the screen as you paste — that's intentional."
|
||||
echo "Paste once, then just press Enter to submit."
|
||||
read -r -s -p "> " value </dev/tty
|
||||
echo
|
||||
|
||||
if [[ -z "$value" ]]; then
|
||||
echo "No input. Aborting." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "$value" =~ ^sk-ant-${prefix} ]]; then
|
||||
echo "Value does not start with sk-ant-${prefix}. Aborting." >&2
|
||||
exit 1
|
||||
fi
|
||||
TOKEN="$value"
|
||||
}
|
||||
|
||||
cat <<EOF
|
||||
How would you like to authenticate?
|
||||
|
||||
1) Use Claude subscription — runs \`claude setup-token\` and saves the
|
||||
resulting token in the Agent Vault.
|
||||
2) I have my own OAuth token — paste an existing sk-ant-oat… token.
|
||||
3) I have my own API key — paste an Anthropic API key (sk-ant-api…).
|
||||
|
||||
EOF
|
||||
|
||||
read -r -p "Choose [1/2/3]: " CHOICE </dev/tty
|
||||
|
||||
case "$CHOICE" in
|
||||
1) capture_via_claude_setup_token ;;
|
||||
2) prompt_for_pasted oat ;;
|
||||
3) prompt_for_pasted api ;;
|
||||
*) echo "Invalid choice." >&2; exit 1 ;;
|
||||
esac
|
||||
if [ -z "$token" ]; then
|
||||
keep=$(mktemp -t claude-setup-token-log.XXXXXX)
|
||||
cp "$tmpfile" "$keep"
|
||||
echo >&2
|
||||
echo "No sk-ant-oat…AA token found. Raw log: $keep" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Got token: ${TOKEN:0:16}…${TOKEN: -4}"
|
||||
echo "Registering with OneCLI as '${SECRET_NAME}' (host pattern: ${HOST_PATTERN})…"
|
||||
echo "Got token: ${token:0:16}…${token: -4}"
|
||||
echo "Saving it to your OneCLI vault as '${SECRET_NAME}' (host: ${HOST_PATTERN})…"
|
||||
|
||||
onecli secrets create \
|
||||
--name "$SECRET_NAME" \
|
||||
--type anthropic \
|
||||
--value "$TOKEN" \
|
||||
--value "$token" \
|
||||
--host-pattern "$HOST_PATTERN"
|
||||
|
||||
echo "Done."
|
||||
|
||||
Reference in New Issue
Block a user