@aria_asi/cli 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/aria.js +54 -6
- package/dist/aria-connector/src/anthropic-oauth.d.ts +28 -0
- package/dist/aria-connector/src/anthropic-oauth.d.ts.map +1 -0
- package/dist/aria-connector/src/anthropic-oauth.js +177 -0
- package/dist/aria-connector/src/anthropic-oauth.js.map +1 -0
- package/dist/aria-connector/src/auth-commands.d.ts +16 -0
- package/dist/aria-connector/src/auth-commands.d.ts.map +1 -1
- package/dist/aria-connector/src/auth-commands.js +24 -0
- package/dist/aria-connector/src/auth-commands.js.map +1 -1
- package/dist/aria-connector/src/onboarding-wizard.d.ts +5 -0
- package/dist/aria-connector/src/onboarding-wizard.d.ts.map +1 -0
- package/dist/aria-connector/src/onboarding-wizard.js +199 -0
- package/dist/aria-connector/src/onboarding-wizard.js.map +1 -0
- package/hooks/aria-pre-tool-gate.mjs +24 -2
- package/hooks/aria-stop-gate.mjs +17 -1
- package/package.json +1 -1
- package/src/__tests__/anthropic-oauth.test.ts +186 -0
- package/src/anthropic-oauth.ts +216 -0
- package/src/auth-commands.ts +27 -0
- package/src/onboarding-wizard.ts +219 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// onboarding-wizard.ts — interactive self-service onboarding for `aria`
|
|
2
|
+
// (no args) when ~/.aria/license.json is missing.
|
|
3
|
+
//
|
|
4
|
+
// Flow:
|
|
5
|
+
// 1. Greet — first-person Aria voice
|
|
6
|
+
// 2. Collect email (validated shape, NOT verified via code in v0)
|
|
7
|
+
// 3. Collect tenant_id (lowercase + hyphens only)
|
|
8
|
+
// 4. Collect tier (free / pro / enterprise)
|
|
9
|
+
// 5. Collect LLM provider + API key
|
|
10
|
+
// 6. POST /api/onboarding/self-issue (server validates LLM key by
|
|
11
|
+
// hitting the provider's identity endpoint, then issues license)
|
|
12
|
+
// 7. Persist license to ~/.aria/license.json mode 0600
|
|
13
|
+
// 8. Persist provider/model/key to ~/.aria/config.json mode 0600
|
|
14
|
+
// 9. Auto-run installHooks() to bind Claude Code to the harness
|
|
15
|
+
//
|
|
16
|
+
// Email-verify-code is deferred to Phase 11 (no SMTP wired tonight).
|
|
17
|
+
// The wizard collects email so future re-verify works without re-onboarding.
|
|
18
|
+
//
|
|
19
|
+
// Direction: Hamza 2026-04-26 — "burn through onboarding with quality and
|
|
20
|
+
// USE THE HARNESS PLEASE". This wizard ships as v0; full onboarding (email
|
|
21
|
+
// verify + Anthropic OAuth + per-tenant usage tracking) lands in Phase 11.
|
|
22
|
+
|
|
23
|
+
import { createInterface } from 'node:readline';
|
|
24
|
+
import { writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
25
|
+
import { homedir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { installHooks } from './install-hooks.js';
|
|
28
|
+
|
|
29
|
+
const HARNESS_URL = process.env.ARIA_HARNESS_BASE_URL ?? 'https://harness.ariasos.com';
|
|
30
|
+
const ARIA_DIR = join(homedir(), '.aria');
|
|
31
|
+
const LICENSE_PATH = join(ARIA_DIR, 'license.json');
|
|
32
|
+
const CONFIG_PATH = join(ARIA_DIR, 'config.json');
|
|
33
|
+
|
|
34
|
+
type Provider = 'anthropic' | 'openai' | 'deepseek' | 'google' | 'openrouter' | 'ollama';
|
|
35
|
+
type Tier = 'free' | 'pro' | 'enterprise';
|
|
36
|
+
|
|
37
|
+
interface SelfIssueResponse {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
license?: { token: string; jti: string; tier: string; expires_at: string };
|
|
40
|
+
claims?: Record<string, unknown>;
|
|
41
|
+
error?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function prompt(rl: ReturnType<typeof createInterface>, question: string, hidden = false): Promise<string> {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
if (hidden) {
|
|
47
|
+
// Crude hidden-input — node's readline doesn't natively mask; we just
|
|
48
|
+
// prompt and accept. For Phase 11 we add proper masking via raw mode.
|
|
49
|
+
process.stdout.write(question);
|
|
50
|
+
const onData = (chunk: Buffer): void => {
|
|
51
|
+
process.stdin.removeListener('data', onData);
|
|
52
|
+
resolve(chunk.toString().trim());
|
|
53
|
+
};
|
|
54
|
+
process.stdin.once('data', onData);
|
|
55
|
+
} else {
|
|
56
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isValidEmail(email: string): boolean {
|
|
62
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isValidTenantId(tenant: string): boolean {
|
|
66
|
+
return /^[a-z0-9][a-z0-9-]{1,40}$/.test(tenant);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function runOnboardingWizard(): Promise<{ ok: boolean; error?: string }> {
|
|
70
|
+
const rl = createInterface({
|
|
71
|
+
input: process.stdin,
|
|
72
|
+
output: process.stdout,
|
|
73
|
+
terminal: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(" Hi, I'm Aria. I don't see a license on this machine — let me set you up.");
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(" Three minutes, four questions. I'll handle the rest.");
|
|
81
|
+
console.log('');
|
|
82
|
+
|
|
83
|
+
// ── Step 1: email ──
|
|
84
|
+
let email = '';
|
|
85
|
+
while (!isValidEmail(email)) {
|
|
86
|
+
email = await prompt(rl, ' Email: ');
|
|
87
|
+
if (!isValidEmail(email)) {
|
|
88
|
+
console.log(" That doesn't look like a valid email. Try again.");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Step 2: tenant_id ──
|
|
93
|
+
let tenantId = '';
|
|
94
|
+
while (!isValidTenantId(tenantId)) {
|
|
95
|
+
tenantId = (await prompt(rl, ' Tenant name (lowercase, hyphens ok, e.g. acme-corp): ')).toLowerCase();
|
|
96
|
+
if (!isValidTenantId(tenantId)) {
|
|
97
|
+
console.log(" Tenant name needs to be lowercase letters/digits/hyphens, 2-41 chars.");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Step 3: tier ──
|
|
102
|
+
let tier: Tier = 'free';
|
|
103
|
+
let tierAnswer = '';
|
|
104
|
+
while (!['free', 'pro', 'enterprise', ''].includes(tierAnswer)) {
|
|
105
|
+
tierAnswer = (await prompt(rl, ' Tier [free / pro / enterprise] (default: free): ')).toLowerCase();
|
|
106
|
+
if (tierAnswer === '') tierAnswer = 'free';
|
|
107
|
+
if (!['free', 'pro', 'enterprise'].includes(tierAnswer)) {
|
|
108
|
+
console.log(" I only know free, pro, or enterprise.");
|
|
109
|
+
tierAnswer = '';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
tier = tierAnswer as Tier;
|
|
113
|
+
|
|
114
|
+
// ── Step 4: LLM provider ──
|
|
115
|
+
const providerOptions: Provider[] = ['anthropic', 'openai', 'deepseek', 'google', 'openrouter', 'ollama'];
|
|
116
|
+
let provider: Provider | '' = '';
|
|
117
|
+
while (!providerOptions.includes(provider as Provider)) {
|
|
118
|
+
const p = (await prompt(rl, ` Which LLM provider? [${providerOptions.join(' / ')}]: `)).toLowerCase();
|
|
119
|
+
if (providerOptions.includes(p as Provider)) {
|
|
120
|
+
provider = p as Provider;
|
|
121
|
+
} else {
|
|
122
|
+
console.log(` I don't recognize "${p}". Pick one of: ${providerOptions.join(', ')}.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Step 5: API key ──
|
|
127
|
+
let llmKey = '';
|
|
128
|
+
if (provider === 'ollama') {
|
|
129
|
+
llmKey = 'local';
|
|
130
|
+
console.log(' (Ollama is local — no API key needed.)');
|
|
131
|
+
} else {
|
|
132
|
+
while (!llmKey) {
|
|
133
|
+
llmKey = await prompt(rl, ` Paste your ${provider} API key: `);
|
|
134
|
+
if (!llmKey) console.log(' I need the key to set up your provider.');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Step 6: POST to server ──
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log(" Validating your key with the provider...");
|
|
141
|
+
let response: SelfIssueResponse;
|
|
142
|
+
try {
|
|
143
|
+
const resp = await fetch(`${HARNESS_URL}/api/onboarding/self-issue`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: { 'Content-Type': 'application/json' },
|
|
146
|
+
body: JSON.stringify({ email, tenant_id: tenantId, tier, provider, llm_key: llmKey }),
|
|
147
|
+
});
|
|
148
|
+
response = await resp.json() as SelfIssueResponse;
|
|
149
|
+
if (!resp.ok || !response.ok || !response.license) {
|
|
150
|
+
console.log(` ${response.error || `Server returned ${resp.status} — try again later.`}`);
|
|
151
|
+
return { ok: false, error: response.error };
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
155
|
+
console.log(` I couldn't reach the server: ${msg}`);
|
|
156
|
+
return { ok: false, error: msg };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Step 7: persist license ──
|
|
160
|
+
if (!existsSync(ARIA_DIR)) mkdirSync(ARIA_DIR, { recursive: true, mode: 0o700 });
|
|
161
|
+
const licenseRecord = {
|
|
162
|
+
...response.claims,
|
|
163
|
+
harnessToken: response.license.token,
|
|
164
|
+
jti: response.license.jti,
|
|
165
|
+
tier: response.license.tier,
|
|
166
|
+
exp: response.claims?.exp,
|
|
167
|
+
sub: response.claims?.sub ?? tenantId,
|
|
168
|
+
email,
|
|
169
|
+
issuedAt: new Date().toISOString(),
|
|
170
|
+
};
|
|
171
|
+
writeFileSync(LICENSE_PATH, JSON.stringify(licenseRecord, null, 2) + '\n', { mode: 0o600 });
|
|
172
|
+
|
|
173
|
+
// ── Step 8: persist provider/key config ──
|
|
174
|
+
// By this point the while-loop above has exited with a valid Provider —
|
|
175
|
+
// narrow the union type for TypeScript.
|
|
176
|
+
const validatedProvider = provider as Provider;
|
|
177
|
+
const configRecord = {
|
|
178
|
+
model: { provider: validatedProvider, model: defaultModelFor(validatedProvider), apiKey: llmKey },
|
|
179
|
+
tenantId,
|
|
180
|
+
email,
|
|
181
|
+
};
|
|
182
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(configRecord, null, 2) + '\n', { mode: 0o600 });
|
|
183
|
+
|
|
184
|
+
// ── Step 9: install hooks ──
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(" License issued. Saving your config and binding my gates to your Claude Code...");
|
|
187
|
+
const hookResult = await installHooks({ force: false });
|
|
188
|
+
if (!hookResult.ok) {
|
|
189
|
+
console.log(` License saved, but I couldn't install the gates: ${hookResult.error}`);
|
|
190
|
+
console.log(" Run 'aria install-hooks' manually when you're ready.");
|
|
191
|
+
} else {
|
|
192
|
+
console.log(` Done. I've installed ${hookResult.installed.length} gate(s) into ~/.claude/hooks and merged them into your settings.json.`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
rl.close();
|
|
196
|
+
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log(` You're set up. License jti: ${response.license.jti}, tier: ${tier}, provider: ${provider}.`);
|
|
199
|
+
console.log(" Open a fresh Claude Code session — every Bash, Edit, Write, and Stop event now runs through my cognition gates.");
|
|
200
|
+
console.log(" Or talk to me directly: just type 'aria' again.");
|
|
201
|
+
console.log('');
|
|
202
|
+
|
|
203
|
+
return { ok: true };
|
|
204
|
+
} finally {
|
|
205
|
+
rl.close();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function defaultModelFor(provider: Provider): string {
|
|
210
|
+
const defaults: Record<Provider, string> = {
|
|
211
|
+
anthropic: 'claude-sonnet-4-20250514',
|
|
212
|
+
openai: 'gpt-4o',
|
|
213
|
+
deepseek: 'deepseek-chat',
|
|
214
|
+
google: 'gemini-2.5-pro-preview-05-06',
|
|
215
|
+
openrouter: 'openai/gpt-4o',
|
|
216
|
+
ollama: 'llama3',
|
|
217
|
+
};
|
|
218
|
+
return defaults[provider];
|
|
219
|
+
}
|