@caveman-ai/cloud 0.0.1 → 0.1.0-rc.22
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/README.md +75 -3
- package/bin/ci.mjs +236 -0
- package/bin/cvm.mjs +261 -27
- package/bin/evals.mjs +621 -0
- package/dist/cloud-client.mjs +34 -33
- package/dist/cloud-mcp.mjs +32 -32
- package/dist/criterion-registry.json +1370 -0
- package/dist/skills/caveman-cloud/SKILL.md +129 -0
- package/dist/skills/caveman-cloud/references/caveman-cloud-compare.md +12 -0
- package/dist/skills/caveman-cloud/references/caveman-cloud-connect.md +54 -0
- package/dist/skills/caveman-cloud/references/caveman-cloud-dashboards.md +72 -0
- package/dist/skills/caveman-cloud/references/caveman-cloud-evals.md +202 -0
- package/dist/skills/caveman-cloud/references/caveman-cloud-project.md +97 -0
- package/dist/skills/caveman-cloud/references/caveman-cloud-scenarios.md +51 -0
- package/dist/skills/caveman-cloud/references/caveman-cloud-setup.md +174 -0
- package/dist/skills/caveman-cloud/references/caveman-cloud-strict-evals.md +52 -0
- package/dist/skills/caveman-cloud/references/caveman-cloud-traces.md +16 -0
- package/dist/skills/caveman-cloud/references/caveman-discover.md +143 -0
- package/dist/skills/caveman-cloud/references/caveman-evidence-review.md +64 -0
- package/dist/skills/caveman-cloud/references/caveman-manage.md +59 -0
- package/dist/skills/caveman-cloud/references/caveman-onboard.md +100 -0
- package/dist/skills/caveman-cloud/references/caveman-optimize.md +113 -0
- package/dist/skills/caveman-cloud/references/caveman-setup.md +282 -0
- package/dist/skills/caveman-cloud/references/caveman-sql.md +92 -0
- package/package.json +7 -4
package/bin/cvm.mjs
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// cvm, the Caveman Cloud CLI.
|
|
3
|
-
// credential
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
import {
|
|
2
|
+
// cvm, the Caveman Cloud CLI. It signs in with the browser device flow and
|
|
3
|
+
// stores the credential exactly where `caveman login` does
|
|
4
|
+
// (~/.caveman-cloud/config.json points at the macOS Keychain item or the 0600
|
|
5
|
+
// credentials file), so either CLI's sign-in serves both, and a refreshed pair
|
|
6
|
+
// goes back to the same place, so neither CLI ever signs the other out.
|
|
7
|
+
import {readFileSync,writeFileSync,mkdirSync,chmodSync,renameSync,existsSync,realpathSync,cpSync,rmSync,readdirSync,statSync} from 'node:fs';
|
|
8
|
+
import {execFileSync,spawn} from 'node:child_process';
|
|
8
9
|
import {randomUUID} from 'node:crypto';
|
|
9
10
|
import {homedir} from 'node:os';
|
|
10
11
|
import {join,dirname,delimiter} from 'node:path';
|
|
12
|
+
import {fileURLToPath} from 'node:url';
|
|
13
|
+
import {parseArgs} from 'node:util';
|
|
14
|
+
import {setTimeout as pause} from 'node:timers/promises';
|
|
11
15
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
16
|
+
const version=JSON.parse(readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
|
|
17
|
+
const client=()=>import('../dist/cloud-client.mjs');
|
|
14
18
|
const configPath=join(homedir(),'.caveman-cloud','config.json');
|
|
15
19
|
const credentialsPath=join(process.env.CAVEMAN_HOME??join(homedir(),'.caveman'),'credentials');
|
|
16
20
|
const keychain=['-s','caveman','-a','token'];
|
|
21
|
+
const origin=url=>(url??'').replace(/\/+$/,'');
|
|
17
22
|
|
|
18
23
|
function readConfig(){try{return JSON.parse(readFileSync(configPath,'utf8'));}catch{return {};}}
|
|
19
24
|
function writePrivate(path,text){
|
|
@@ -30,7 +35,8 @@ function storedSecret(meta){
|
|
|
30
35
|
}
|
|
31
36
|
// A stored secret is the credentials JSON, or a bare access token in older stores.
|
|
32
37
|
function decode(raw){try{const parsed=JSON.parse(raw);if(typeof parsed.access_token==='string')return parsed;}catch{}return {access_token:raw};}
|
|
33
|
-
|
|
38
|
+
const claims=token=>{try{return JSON.parse(Buffer.from(token.split('.')[0],'base64url').toString('utf8'))??{};}catch{return {};}};
|
|
39
|
+
const expiresSoon=token=>claims(token).exp<=Date.now()/1000+60;
|
|
34
40
|
function store(credentials){
|
|
35
41
|
const secret=JSON.stringify(credentials);
|
|
36
42
|
if(process.platform==='darwin'&&!process.env.CAVE_NO_KEYCHAIN){try{execFileSync('security',['add-generic-password','-U',...keychain,'-w',secret],{stdio:'ignore'});return 'keychain';}catch{}}
|
|
@@ -58,34 +64,262 @@ async function refresh(meta,credentials,baseURL){
|
|
|
58
64
|
}catch{return credentials;}
|
|
59
65
|
}
|
|
60
66
|
async function context(){
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
// The environment wins over the saved login, and a flag over both (--project,
|
|
68
|
+
// read by the client). logout leaves baseURL empty.
|
|
69
|
+
const meta=readConfig(),baseURL=process.env.CAVE_API_URL||meta.baseURL||'https://api.caveman.so';
|
|
70
|
+
const projectId=process.env.CAVE_PROJECT_ID||meta.projectId;
|
|
71
|
+
if(process.env.CAVE_TOKEN)return {baseURL,token:process.env.CAVE_TOKEN,projectId};
|
|
72
|
+
// The saved credential was issued by the saved address and is sent nowhere
|
|
73
|
+
// else: another CAVE_API_URL needs its own CAVE_TOKEN.
|
|
74
|
+
if(meta.baseURL&&origin(baseURL)!==origin(meta.baseURL))return {baseURL,token:undefined,projectId};
|
|
63
75
|
let credentials=decode(storedSecret(meta));
|
|
64
76
|
if(!meta.logoutPendingLocalCleanup&&credentials.refresh_token&&expiresSoon(credentials.access_token))credentials=await refresh(meta,credentials,baseURL);
|
|
65
|
-
return {baseURL,token:credentials.access_token,projectId:
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
return {baseURL,token:credentials.access_token,projectId:projectId??credentials.project_id};
|
|
78
|
+
}
|
|
79
|
+
const post=(url,body,headers={})=>fetch(url,{method:'POST',redirect:'error',headers:{'content-type':'application/json','x-cave-client':'cli',...headers},body:JSON.stringify(body),signal:AbortSignal.timeout(10000)});
|
|
80
|
+
function openBrowser(url){
|
|
81
|
+
const [command,args]=process.platform==='darwin'?['open',[url]]:process.platform==='win32'?['rundll32.exe',['url.dll,FileProtocolHandler',url]]:['xdg-open',[url]];
|
|
82
|
+
try{spawn(command,args,{detached:true,stdio:'ignore'}).on('error',()=>{}).unref();}catch{}
|
|
83
|
+
}
|
|
84
|
+
// RFC 8628 device grant: the requests `caveman login` makes, and the
|
|
85
|
+
// credential stored where it stores one.
|
|
86
|
+
async function login(args=[]){
|
|
87
|
+
const {CloudError}=await client();
|
|
88
|
+
let values;try{({values}=parseArgs({args,strict:true,options:{instance:{type:'string'},'no-browser':{type:'boolean'}}}));}catch{throw new CloudError('cave_usage','Use cvm login [--instance URL] [--no-browser].',2);}
|
|
89
|
+
const baseURL=origin(values.instance||process.env.CAVE_API_URL||readConfig().baseURL||'https://api.caveman.so');
|
|
90
|
+
let url;try{url=new URL(baseURL);}catch{}
|
|
91
|
+
if(!url||url.username||url.password||url.pathname!=='/'||url.search||url.hash||url.protocol!=='https:'&&!(url.protocol==='http:'&&['localhost','127.0.0.1','[::1]'].includes(url.hostname)))throw new CloudError('cave_endpoint_invalid','Sign in over HTTPS (plain HTTP only on localhost), to an origin with no path, credentials, query or fragment.',2);
|
|
92
|
+
const started=await post(`${baseURL}/api/v1/auth/device/code`,{}).catch(()=>{throw new CloudError('cave_network_error',`Could not reach ${baseURL}.`,1);});
|
|
93
|
+
const code=await started.json().catch(()=>({}));
|
|
94
|
+
if(started.status===403&&code.error?.code==='cave_device_login_disabled')throw new CloudError('cave_device_login_disabled','Device login is not open on this installation.',3);
|
|
95
|
+
if(!started.ok||typeof code.device_code!=='string')throw new CloudError(code.error?.code??'cave_device_login_failed',`Could not start sign-in at ${baseURL} (HTTP ${started.status}).`,1);
|
|
96
|
+
const link=code.verification_uri_complete??code.verification_uri;
|
|
97
|
+
process.stderr.write(`Confirm the code ${code.user_code} in your browser: ${link}\n`);
|
|
98
|
+
if(!values['no-browser']&&!process.env.CAVE_NO_BROWSER&&/^https?:\/\//.test(link))openBrowser(link);
|
|
99
|
+
let interval=Number(code.interval??5)*1000;const deadline=Date.now()+Number(code.expires_in??600)*1000;
|
|
100
|
+
const expired=new CloudError('expired_token','The code expired before it was confirmed. Run cvm login again.',3);
|
|
101
|
+
while(Date.now()<deadline){
|
|
102
|
+
await pause(Math.max(interval,200));
|
|
103
|
+
let response,grant;
|
|
104
|
+
// A dropped poll, a rate limit or a busy server is retried until the code expires.
|
|
105
|
+
try{response=await post(`${baseURL}/api/v1/auth/device/token`,{device_code:code.device_code});if(response.status>=500||response.status===429)continue;grant=await response.json();}catch{continue;}
|
|
106
|
+
if(grant.error==='slow_down'){interval+=5000;continue;}
|
|
107
|
+
if(grant.error==='authorization_pending')continue;
|
|
108
|
+
if(grant.error==='expired_token')throw expired;
|
|
109
|
+
if(grant.error==='access_denied')throw new CloudError('access_denied','Sign-in was denied in the browser.',3);
|
|
110
|
+
if(typeof grant.access_token!=='string'||!grant.access_token)throw new CloudError(typeof grant.error==='string'?grant.error:'cave_device_login_failed',`Sign-in failed: ${typeof grant.error==='string'?grant.error:`HTTP ${response.status}`}.`,1);
|
|
111
|
+
const credentials=Object.fromEntries(['access_token','refresh_token','gateway_api_key','gateway_key_id','project_id'].filter(key=>typeof grant[key]==='string'&&grant[key]).map(key=>[key,grant[key]]));
|
|
112
|
+
const tokenStore=store(credentials),{oid}=claims(credentials.access_token);
|
|
113
|
+
// No gatewayUrl: with one, `caveman enable claude` switches to managed mode
|
|
114
|
+
// and writes a gateway base URL with no key into Claude Code's settings.
|
|
115
|
+
const {token,logoutPendingLocalCleanup,gatewayUrl,organizationId,projectId,...rest}=readConfig();
|
|
116
|
+
writePrivate(configPath,JSON.stringify({...rest,baseURL,tokenStore,...(typeof oid==='string'?{organizationId:oid}:{}),...(credentials.project_id?{projectId:credentials.project_id}:{})},null,2)+'\n');
|
|
117
|
+
// The receipt: the server replays a durable grant until told it was stored,
|
|
118
|
+
// and may revoke one that never is.
|
|
119
|
+
if(Object.keys(credentials).length>1){
|
|
120
|
+
for(let attempt=0;;attempt++){
|
|
121
|
+
const ack=typeof grant.delivery_ack_token==='string'&&await post(`${baseURL}/api/v1/auth/device/ack`,{device_code:code.device_code,ack_token:grant.delivery_ack_token},{authorization:`Bearer ${credentials.access_token}`}).catch(()=>undefined);
|
|
122
|
+
if(ack?.ok)break;
|
|
123
|
+
if(attempt===4||ack===false||ack&&ack.status<500&&ack.status!==429)throw new CloudError('cave_device_ack_failed','Signed in, but the server did not confirm this device stored the credential and may revoke it. Run cvm login again.',1);
|
|
124
|
+
await pause(200*2**attempt);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {baseURL,projectId:credentials.project_id??null};
|
|
128
|
+
}
|
|
129
|
+
throw expired;
|
|
130
|
+
}
|
|
131
|
+
async function loginCommand(args){
|
|
132
|
+
const {baseURL,projectId}=await login(args);
|
|
133
|
+
if(process.stdout.isTTY)process.stdout.write(`Signed in to ${baseURL}.\n`);
|
|
134
|
+
else{process.stderr.write(`Signed in to ${baseURL}.\n`);process.stdout.write(JSON.stringify({signed_in:true,api:baseURL,project_id:projectId})+'\n');}
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
// `caveman logout`'s steps: revoke the device's gateway key and the session,
|
|
138
|
+
// then forget the credential and the address.
|
|
139
|
+
async function logout(){
|
|
140
|
+
const meta=readConfig(),baseURL=origin(meta.baseURL);
|
|
141
|
+
let credentials=decode(storedSecret(meta));
|
|
142
|
+
if(credentials.access_token&&baseURL&&!meta.logoutPendingLocalCleanup){
|
|
143
|
+
const {CloudError}=await client();
|
|
144
|
+
if(credentials.refresh_token&&expiresSoon(credentials.access_token))credentials=await refresh(meta,credentials,baseURL);
|
|
145
|
+
const kept=new CloudError('cave_logout_failed','Could not revoke this sign-in on the server; you are still signed in. Run cvm logout again.',1);
|
|
146
|
+
const revoke=(path,body,headers)=>post(baseURL+path,body,{authorization:`Bearer ${credentials.access_token}`,...headers}).catch(()=>{throw kept;});
|
|
147
|
+
const project=meta.projectId??credentials.project_id;
|
|
148
|
+
// The gateway key is gone only when the server says so (404 cave_key_not_found):
|
|
149
|
+
// after any other answer this is the last copy of a live key, so it stays.
|
|
150
|
+
if(project&&credentials.gateway_key_id){
|
|
151
|
+
const response=await revoke(`/api/v1/projects/${encodeURIComponent(project)}/keys/${encodeURIComponent(credentials.gateway_key_id)}/revoke`,{},{'x-cave-csrf':'cli'});
|
|
152
|
+
if(!response.ok&&!(response.status===404&&(await response.json().catch(()=>({}))).error?.code==='cave_key_not_found'))throw kept;
|
|
153
|
+
}
|
|
154
|
+
const response=await revoke('/api/v1/auth/logout',credentials.refresh_token?{refresh_token:credentials.refresh_token}:{});
|
|
155
|
+
if(!response.ok&&response.status!==401)throw kept; // 401: the session already ended
|
|
156
|
+
}
|
|
157
|
+
// Remote revocation is recorded before the only local credential goes, so a
|
|
158
|
+
// failed cleanup is retried without presenting a revoked session.
|
|
159
|
+
writePrivate(configPath,JSON.stringify({...meta,logoutPendingLocalCleanup:true},null,2)+'\n');
|
|
160
|
+
if(meta.tokenStore==='keychain'||!meta.tokenStore&&process.platform==='darwin'&&!process.env.CAVE_NO_KEYCHAIN){
|
|
161
|
+
try{execFileSync('security',['delete-generic-password',...keychain],{stdio:'ignore'});}catch(error){if(error.status!==44)throw new Error('Could not remove the credential from the macOS Keychain.');} // 44: no such item
|
|
162
|
+
}
|
|
163
|
+
if(meta.tokenStore!=='keychain')rmSync(credentialsPath,{force:true});
|
|
164
|
+
const {token,tokenStore,projectId,organizationId,gatewayUrl,logoutPendingLocalCleanup,...rest}=meta;
|
|
165
|
+
writePrivate(configPath,JSON.stringify({...rest,baseURL:''},null,2)+'\n');
|
|
166
|
+
process.stdout.write(process.stdout.isTTY?'Signed out.\n':JSON.stringify({logged_out:true})+'\n');
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The Caveman Cloud skill, built from public/skills into this package. Claude
|
|
171
|
+
// Code reads its own skills directory; Codex and Cursor read the open Agent
|
|
172
|
+
// Skills one.
|
|
173
|
+
const skillDirs={claude:join(process.env.CLAUDE_CONFIG_DIR||join(homedir(),'.claude'),'skills','caveman-cloud'),agents:join(homedir(),'.agents','skills','caveman-cloud')};
|
|
174
|
+
const marker='.cvm-version',labels={claude:'Claude Code',codex:'Codex',cursor:'Cursor'};
|
|
175
|
+
// One cvm at a time changes a skills directory, and first clears what an
|
|
176
|
+
// interrupted one left. A lock over a minute old is a crashed cvm's (two cvms
|
|
177
|
+
// taking over the same stale lock at one moment can both go ahead).
|
|
178
|
+
function locked(dir,work){
|
|
179
|
+
const parent=dirname(dir),lock=join(parent,'.caveman-cloud.lock'),busy='another cvm is installing it';
|
|
180
|
+
try{mkdirSync(lock);}catch(error){
|
|
181
|
+
if(error.code!=='EEXIST')throw error;
|
|
182
|
+
if(Date.now()-statSync(lock).mtimeMs<60000)return busy;
|
|
183
|
+
rmSync(lock,{recursive:true,force:true});
|
|
184
|
+
try{mkdirSync(lock);}catch{return busy;}
|
|
185
|
+
}
|
|
186
|
+
try{
|
|
187
|
+
for(const name of readdirSync(parent))if(name.startsWith('.caveman-cloud-'))rmSync(join(parent,name),{recursive:true,force:true});
|
|
188
|
+
return work();
|
|
189
|
+
}finally{rmSync(lock,{recursive:true,force:true});}
|
|
190
|
+
}
|
|
191
|
+
// Only a directory cvm wrote (its marker says so) is replaced, by renames: an
|
|
192
|
+
// agent reading it finds the old skill or the new one, never half of either.
|
|
193
|
+
function installSkill(dir){
|
|
194
|
+
mkdirSync(dirname(dir),{recursive:true});
|
|
195
|
+
return locked(dir,()=>{
|
|
196
|
+
if(existsSync(dir)&&!existsSync(join(dir,marker)))return 'it holds a caveman-cloud skill cvm did not install';
|
|
197
|
+
const next=join(dirname(dir),`.caveman-cloud-${randomUUID()}`),old=next+'-old';
|
|
198
|
+
try{
|
|
199
|
+
cpSync(fileURLToPath(new URL('../dist/skills/caveman-cloud',import.meta.url)),next,{recursive:true});writeFileSync(join(next,marker),version);
|
|
200
|
+
if(existsSync(dir))renameSync(dir,old);
|
|
201
|
+
renameSync(next,dir);
|
|
202
|
+
}catch(error){
|
|
203
|
+
rmSync(next,{recursive:true,force:true});
|
|
204
|
+
if(!existsSync(dir)&&existsSync(old))renameSync(old,dir);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
rmSync(old,{recursive:true,force:true});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// Runs a client command for its JSON answer, the client's own printing held back.
|
|
211
|
+
async function call(...args){
|
|
212
|
+
const {runCloudCLI}=await client();
|
|
213
|
+
let out='';const write=process.stdout.write;process.stdout.write=chunk=>(out+=chunk,true);
|
|
214
|
+
try{const code=await runCloudCLI([...args,'--format','json'],{context,login:loginCommand});return {...JSON.parse(out),code};}
|
|
215
|
+
finally{process.stdout.write=write;}
|
|
216
|
+
}
|
|
217
|
+
// What the onboarding screen shows of the repository, each only when git
|
|
218
|
+
// answers it in a form the server accepts.
|
|
219
|
+
function repository(){
|
|
220
|
+
const git=(...args)=>{try{return execFileSync('git',args,{encoding:'utf8',stdio:['ignore','pipe','ignore'],timeout:5000}).trim();}catch{return '';}};
|
|
221
|
+
const [,owner,name]=git('remote','get-url','origin').match(/([^/:]+)\/([^/]+?)(?:\.git)?\/?$/)??[];
|
|
222
|
+
const facts={repo_owner:owner,repo_name:name,default_branch:git('symbolic-ref','--short','refs/remotes/origin/HEAD').replace(/^origin\//,'')||git('branch','--show-current'),head_sha:git('rev-parse','HEAD')};
|
|
223
|
+
const valid={repo_owner:/^[A-Za-z0-9._-]{1,100}$/,repo_name:/^[A-Za-z0-9._-]{1,100}$/,default_branch:/^(?!-)(?!.*\.\.)[^\s~^:?*[\\\x00-\x1f\x7f]{1,255}$/,head_sha:/^[0-9a-f]{7,64}$/};
|
|
224
|
+
return Object.entries(facts).filter(([key,value])=>value&&valid[key].test(value));
|
|
225
|
+
}
|
|
226
|
+
async function setup(args){
|
|
227
|
+
const {CloudError}=await client();
|
|
228
|
+
const usage='Use cvm setup [--project UUID] [--agent claude|codex|cursor] [--remove] [--format json|table].';
|
|
229
|
+
let values;try{({values}=parseArgs({args,strict:true,options:{project:{type:'string'},agent:{type:'string'},remove:{type:'boolean'},format:{type:'string'}}}));}catch{throw new CloudError('cave_usage',usage,2);}
|
|
230
|
+
const json=(values.format??(process.stdout.isTTY?'table':'json'))==='json';
|
|
231
|
+
if(values.format&&!['json','table'].includes(values.format)||values.agent&&!labels[values.agent])throw new CloudError('cave_usage',usage,2);
|
|
232
|
+
const plain=text=>String(text).replace(/[\x00-\x1f\x7f-\x9f\p{Cf}\u2028\u2029]/gu,' ');
|
|
233
|
+
const out=(value,lines)=>process.stdout.write((json?JSON.stringify(value):lines.join('\n'))+'\n');
|
|
234
|
+
const failed=({code,...result})=>(process.stdout.write(JSON.stringify(result)+'\n'),code);
|
|
235
|
+
if(values.remove){
|
|
236
|
+
const removed=Object.values(skillDirs).filter(dir=>existsSync(dirname(dir))&&locked(dir,()=>existsSync(join(dir,marker))&&(rmSync(dir,{recursive:true,force:true}),true))===true);
|
|
237
|
+
out({skill:{removed}},[...removed.map(dir=>`Removed ${dir}`),...removed.length?[]:['No Caveman Cloud skill installed by cvm.'],'cvm logout signs you out.']);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
// 1. A credential that answers, or a sign-in.
|
|
241
|
+
let me=await call('auth','status');
|
|
242
|
+
if(me.code===3&&!process.env.CAVE_TOKEN){await login();me=await call('auth','status');}
|
|
243
|
+
if(me.code)return failed(me);
|
|
244
|
+
// 2. The project, resolved as every cvm command resolves it, saved the way
|
|
245
|
+
// `cvm context set` saves it.
|
|
246
|
+
const shown=await call('context','show',...(values.project?['--project',values.project]:[]));
|
|
247
|
+
if(shown.code)return failed(shown);
|
|
248
|
+
let project;
|
|
249
|
+
if(shown.project_id){const got=await call('projects','get',shown.project_id);if(got.code)return failed(got);project=got.data;}
|
|
250
|
+
else{
|
|
251
|
+
const list=await call('projects','list');if(list.code)return failed(list);
|
|
252
|
+
const projects=(list.data.data??[]).map(({id,name})=>({id,name}));
|
|
253
|
+
if(projects.length!==1){
|
|
254
|
+
out({schema_version:1,ok:false,status:'failed',error:{code:'cave_project_required',message:`This sign-in reaches ${projects.length} projects: pass --project UUID.`},projects},[`This sign-in reaches ${projects.length} projects: pass --project UUID.`,...projects.map(p=>` ${p.id} ${plain(p.name)}`)]);
|
|
255
|
+
return 2;
|
|
256
|
+
}
|
|
257
|
+
project=projects[0];
|
|
258
|
+
}
|
|
259
|
+
const saved=await call('context','set',project.id);if(saved.code)return failed(saved);
|
|
260
|
+
// 3. The skill, for the agent named, or for every agent found here.
|
|
261
|
+
const has=name=>(process.env.PATH??'').split(delimiter).some(dir=>dir&&['','.cmd','.exe'].some(ext=>existsSync(join(dir,name+ext))));
|
|
262
|
+
const found=values.agent?[values.agent]:[['claude',dirname(dirname(skillDirs.claude))],['codex',join(homedir(),'.codex')],['cursor',join(homedir(),'.cursor')]].filter(([name,dir])=>has(name)||existsSync(dir)).map(([name])=>name);
|
|
263
|
+
const agents=found.length?found:['codex','cursor']; // none found: the open directory, which both read
|
|
264
|
+
const dirOf=agent=>agent==='claude'?skillDirs.claude:skillDirs.agents,outcome=new Map();
|
|
265
|
+
for(const dir of new Set(agents.map(dirOf))){try{outcome.set(dir,installSkill(dir));}catch(error){outcome.set(dir,error.message);}}
|
|
266
|
+
const installed=agents.filter(agent=>!outcome.get(dirOf(agent))).map(agent=>({agent,path:dirOf(agent)}));
|
|
267
|
+
const refused=[...outcome].filter(([,reason])=>reason).map(([path,reason])=>({path,reason}));
|
|
268
|
+
// 4. The onboarding screen follows along; it knows the two clients below.
|
|
269
|
+
// Best effort: no report is no failure.
|
|
270
|
+
const reporter=values.agent??found.find(agent=>agent!=='cursor');
|
|
271
|
+
let reported=false;
|
|
272
|
+
if(['claude','codex'].includes(reporter)&&installed.length&&!refused.length){
|
|
273
|
+
try{
|
|
274
|
+
const session=await call('wizard','start','--client',reporter,...repository().map(([key,value])=>`--${key.replaceAll('_','-')}=${value}`),'--project',project.id);
|
|
275
|
+
const tasks=[['sign_in','Sign in',me.data.user.email||new URL(shown.endpoint).host],['project','Choose the project',project.name],['skill','Install the Caveman Cloud skill',[...new Set(installed.map(i=>labels[i.agent]))].join(', ')]].map(([id,label,detail])=>({id,label,status:'done',detail:String(detail).slice(0,240)}));
|
|
276
|
+
reported=!session.code&&!(await call('wizard','update',session.data.id,'--phase','completed','--tasks',JSON.stringify(tasks),'--project',project.id)).code;
|
|
277
|
+
}catch{}
|
|
278
|
+
}
|
|
279
|
+
const next=`Open ${labels[reporter??found[0]]??'your coding agent'} in your repository and ask: "Set up Caveman Cloud for this repository."`;
|
|
280
|
+
out({ok:!refused.length,signed_in:true,api:shown.endpoint,project:{id:project.id,name:project.name},skill:{version,installed,refused},onboarding_reported:reported,next},[
|
|
281
|
+
`Signed in as ${plain(me.data.user.email)} at ${shown.endpoint}`,`Project ${plain(project.name)} (${project.id})`,
|
|
282
|
+
...[...new Set(installed.map(i=>i.path))].map(path=>`Installed the Caveman Cloud skill for ${installed.filter(i=>i.path===path).map(i=>labels[i.agent]).join(' and ')}: ${path}`),
|
|
283
|
+
...refused.map(r=>`Left ${r.path} alone: ${r.reason}`),'',next]);
|
|
284
|
+
return refused.length?1:0;
|
|
76
285
|
}
|
|
77
286
|
|
|
78
287
|
const argv=process.argv.slice(2);
|
|
288
|
+
// What only the operation form of `evals validate` and `evals run` takes.
|
|
289
|
+
// A bare id is the experiment to run, unless it is the value of --project.
|
|
290
|
+
const operationInput=(arg,i,args)=>/^--(file|stdin|workbench-id|scope|concurrency|outputs|wait|wait-timeout|idempotency-key|out)(=|$)/.test(arg)||args[i-1]!=='--project'&&/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(arg);
|
|
291
|
+
// `cvm setup` with flags or nothing is onboarding; `cvm setup show|assert|hold` are operations.
|
|
292
|
+
const onboarding=argv[0]==='setup'&&!(argv[1]&&!argv[1].startsWith('-'));
|
|
293
|
+
// A skill cvm installed follows the installed cvm: an upgrade refreshes it on
|
|
294
|
+
// the next command. Nothing is created here, and nothing here fails a command.
|
|
295
|
+
if(!['--version','-v'].includes(argv[0])&&!(onboarding&&argv.includes('--remove'))){
|
|
296
|
+
for(const dir of Object.values(skillDirs))try{if(readFileSync(join(dir,marker),'utf8')!==version)installSkill(dir);}catch{}
|
|
297
|
+
}
|
|
79
298
|
if(argv[0]==='--version'||argv[0]==='-v'){
|
|
80
|
-
process.stdout.write(
|
|
81
|
-
}else if(argv[0]
|
|
82
|
-
|
|
299
|
+
process.stdout.write(version+'\n');
|
|
300
|
+
}else if(['login','logout'].includes(argv[0])||onboarding){
|
|
301
|
+
// No TTY needed: a coding agent runs these through its shell, and the person
|
|
302
|
+
// confirms the sign-in in the browser.
|
|
303
|
+
try{process.exitCode=await {login:loginCommand,logout,setup}[argv[0]](argv.slice(1));}
|
|
304
|
+
catch(error){
|
|
305
|
+
const exit=error.exitCode??1;
|
|
306
|
+
process.stdout.write(JSON.stringify({schema_version:1,ok:false,status:'failed',error:{code:error.exitCode?error.code:'cave_failed',message:error.message}})+'\n');
|
|
307
|
+
process.exitCode=exit;
|
|
308
|
+
}
|
|
83
309
|
}else if(argv[0]==='mcp'&&argv.length===1){
|
|
84
310
|
const {serveCloudMCP}=await import('../dist/cloud-mcp.mjs');
|
|
85
311
|
// A launcher may narrow the server to the operations one task needs.
|
|
86
312
|
const granted=(process.env.CAVE_MCP_OPERATIONS??'').split(',').map(id=>id.trim()).filter(Boolean);
|
|
87
313
|
await serveCloudMCP({context,...(granted.length?{grantedOperations:granted}:{})});
|
|
314
|
+
}else if(argv[0]==='evals'&&(argv[1]==='init'||['validate','run'].includes(argv[1])&&!argv.slice(2).some(operationInput))){
|
|
315
|
+
// An eval file (*.eval.json) from nothing to a gated run. `evals validate
|
|
316
|
+
// --file MANIFEST` and `evals run WORKBENCH_ID` stay the client's own.
|
|
317
|
+
const evals=await import('./evals.mjs');
|
|
318
|
+
process.exitCode=await {init:evals.evalsInit,validate:evals.evalsValidate,run:evals.evalsRun}[argv[1]](argv.slice(2),{context});
|
|
319
|
+
}else if(argv[0]==='evals'&&['ci','watch'].includes(argv[1])){
|
|
320
|
+
const {evalsCI,evalsWatch}=await import('./ci.mjs');
|
|
321
|
+
process.exitCode=await (argv[1]==='ci'?evalsCI:evalsWatch)(argv.slice(2),{context,login:loginCommand});
|
|
88
322
|
}else{
|
|
89
|
-
const {runCloudCLI}=await
|
|
90
|
-
process.exitCode=await runCloudCLI(argv,{context,login});
|
|
323
|
+
const {runCloudCLI}=await client();
|
|
324
|
+
process.exitCode=await runCloudCLI(argv,{context,login:loginCommand});
|
|
91
325
|
}
|