mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
01389ff8fc
Aggregates the loose OneCLI install, secret registration, and first-agent wiring commands from /setup into three new dispatcher steps. Adds --cli-only mode to init-first-agent so /new-setup can reach a working 2-way CLI chat with the bare minimum. - setup/onecli.ts: idempotent install + PATH + api-host + .env, polls /health - setup/auth.ts: --check verifies secret; --create --value registers it - setup/cli-agent.ts: wraps init-first-agent --cli-only - scripts/init-first-agent.ts: --cli-only mode; DM mode unchanged Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
187 lines
4.8 KiB
TypeScript
187 lines
4.8 KiB
TypeScript
/**
|
|
* Step: auth — Verify or register an Anthropic credential in OneCLI.
|
|
*
|
|
* Modes:
|
|
* --check (default) Verify an Anthropic secret exists.
|
|
* --create --value <token> Create an Anthropic secret. Errors if one
|
|
* already exists unless --force is passed.
|
|
*
|
|
* The actual user-facing prompt (subscription vs API key, paste the token)
|
|
* stays in the /new-setup SKILL.md. This step is just the machine side:
|
|
* it calls `onecli secrets list` / `onecli secrets create` and emits a
|
|
* structured status block. The token value is never logged.
|
|
*/
|
|
import { execFileSync } from 'child_process';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
|
|
import { log } from '../src/log.js';
|
|
import { emitStatus } from './status.js';
|
|
|
|
const LOCAL_BIN = path.join(os.homedir(), '.local', 'bin');
|
|
|
|
interface Args {
|
|
mode: 'check' | 'create';
|
|
value?: string;
|
|
force: boolean;
|
|
}
|
|
|
|
function childEnv(): NodeJS.ProcessEnv {
|
|
const parts = [LOCAL_BIN];
|
|
if (process.env.PATH) parts.push(process.env.PATH);
|
|
return { ...process.env, PATH: parts.join(path.delimiter) };
|
|
}
|
|
|
|
function parseArgs(args: string[]): Args {
|
|
let mode: 'check' | 'create' = 'check';
|
|
let value: string | undefined;
|
|
let force = false;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const key = args[i];
|
|
const val = args[i + 1];
|
|
switch (key) {
|
|
case '--check':
|
|
mode = 'check';
|
|
break;
|
|
case '--create':
|
|
mode = 'create';
|
|
break;
|
|
case '--value':
|
|
value = val;
|
|
i++;
|
|
break;
|
|
case '--force':
|
|
force = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (mode === 'create' && !value) {
|
|
emitStatus('AUTH', {
|
|
STATUS: 'failed',
|
|
ERROR: 'missing_value_for_create',
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
process.exit(2);
|
|
}
|
|
|
|
return { mode, value, force };
|
|
}
|
|
|
|
interface OnecliSecret {
|
|
id: string;
|
|
name: string;
|
|
type: string;
|
|
hostPattern: string | null;
|
|
}
|
|
|
|
function listSecrets(): OnecliSecret[] {
|
|
const out = execFileSync('onecli', ['secrets', 'list'], {
|
|
encoding: 'utf-8',
|
|
env: childEnv(),
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
});
|
|
const parsed = JSON.parse(out) as { data?: unknown };
|
|
return Array.isArray(parsed.data) ? (parsed.data as OnecliSecret[]) : [];
|
|
}
|
|
|
|
function findAnthropicSecret(secrets: OnecliSecret[]): OnecliSecret | undefined {
|
|
return secrets.find((s) => s.type === 'anthropic');
|
|
}
|
|
|
|
function createAnthropicSecret(value: string): void {
|
|
// `value` is a credential — do not log it, do not echo, do not pass through a shell.
|
|
execFileSync(
|
|
'onecli',
|
|
[
|
|
'secrets',
|
|
'create',
|
|
'--name',
|
|
'Anthropic',
|
|
'--type',
|
|
'anthropic',
|
|
'--value',
|
|
value,
|
|
'--host-pattern',
|
|
'api.anthropic.com',
|
|
],
|
|
{
|
|
env: childEnv(),
|
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function run(args: string[]): Promise<void> {
|
|
const { mode, value, force } = parseArgs(args);
|
|
|
|
let secrets: OnecliSecret[];
|
|
try {
|
|
secrets = listSecrets();
|
|
} catch (err) {
|
|
log.error('onecli secrets list failed', { err });
|
|
emitStatus('AUTH', {
|
|
STATUS: 'failed',
|
|
ERROR: 'onecli_list_failed',
|
|
HINT: 'Is OneCLI running? Run `/new-setup` from the onecli step.',
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
const existing = findAnthropicSecret(secrets);
|
|
|
|
if (mode === 'check') {
|
|
emitStatus('AUTH', {
|
|
SECRET_PRESENT: !!existing,
|
|
ANTHROPIC_OK: !!existing,
|
|
STATUS: existing ? 'success' : 'missing',
|
|
...(existing ? { SECRET_NAME: existing.name, SECRET_ID: existing.id } : {}),
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// mode === 'create'
|
|
if (existing && !force) {
|
|
emitStatus('AUTH', {
|
|
SECRET_PRESENT: true,
|
|
STATUS: 'skipped',
|
|
REASON: 'anthropic_secret_already_exists',
|
|
SECRET_NAME: existing.name,
|
|
SECRET_ID: existing.id,
|
|
HINT: 'Re-run with --force to replace, or delete the existing secret first.',
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
createAnthropicSecret(value!);
|
|
} catch (err) {
|
|
const e = err as { stderr?: string | Buffer; status?: number };
|
|
const stderr = typeof e.stderr === 'string' ? e.stderr : e.stderr?.toString('utf-8') ?? '';
|
|
log.error('onecli secrets create failed', { status: e.status, stderr });
|
|
emitStatus('AUTH', {
|
|
STATUS: 'failed',
|
|
ERROR: 'onecli_create_failed',
|
|
EXIT_CODE: e.status ?? -1,
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
// Re-verify
|
|
const updated = findAnthropicSecret(listSecrets());
|
|
|
|
emitStatus('AUTH', {
|
|
SECRET_PRESENT: !!updated,
|
|
ANTHROPIC_OK: !!updated,
|
|
CREATED: true,
|
|
STATUS: updated ? 'success' : 'failed',
|
|
...(updated ? { SECRET_NAME: updated.name, SECRET_ID: updated.id } : {}),
|
|
LOG: 'logs/setup.log',
|
|
});
|
|
}
|