@caveman-ai/cloud 0.1.0-rc.25 → 0.1.0-rc.26

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  Every Caveman Cloud operation from the terminal, plus the Cloud MCP server.
4
4
 
5
5
  ```sh
6
- npm i -g "@caveman-ai/cloud@>=0.1.0-rc.22" # the first release with setup; `latest` is still 0.0.1
6
+ npm i -g @caveman-ai/cloud@rc # the newest release; `latest` is still the 0.0.1 bootstrap
7
7
  cvm setup --project PROJECT_ID # signs in, picks the project, installs the Caveman Cloud skill
8
8
  ```
9
9
 
package/bin/cvm.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
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.
3
+ // stores the credential exactly where `caveman login` does (the Cloud config
4
+ // points at the macOS Keychain item or the 0600 credentials file), so either
5
+ // CLI's sign-in serves both, and a refreshed pair goes back to the same place,
6
+ // so neither CLI ever signs the other out.
7
7
  import {readFileSync,writeFileSync,mkdirSync,chmodSync,renameSync,existsSync,realpathSync,cpSync,rmSync,readdirSync,statSync} from 'node:fs';
8
8
  import {execFileSync,spawn} from 'node:child_process';
9
9
  import {randomUUID} from 'node:crypto';
@@ -15,12 +15,17 @@ import {setTimeout as pause} from 'node:timers/promises';
15
15
 
16
16
  const version=JSON.parse(readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
17
17
  const client=()=>import('../dist/cloud-client.mjs');
18
- const configPath=join(homedir(),'.caveman-cloud','config.json');
19
- const credentialsPath=join(process.env.CAVEMAN_HOME??join(homedir(),'.caveman'),'credentials');
18
+ const cavemanHome=process.env.CAVEMAN_HOME??join(homedir(),'.caveman');
19
+ const credentialsPath=join(cavemanHome,'credentials');
20
+ // The Cloud config is $CAVEMAN_HOME/cloud.json, as `caveman` (v4) keeps it; that
21
+ // CLI copies the old ~/.caveman-cloud/config.json there once, and until then the
22
+ // old file is the one both read and write.
23
+ const legacyConfigPath=join(homedir(),'.caveman-cloud','config.json');
24
+ function configPath(){const path=join(cavemanHome,'cloud.json');return existsSync(path)||!existsSync(legacyConfigPath)?path:legacyConfigPath;}
20
25
  const keychain=['-s','caveman','-a','token'];
21
26
  const origin=url=>(url??'').replace(/\/+$/,'');
22
27
 
23
- function readConfig(){try{return JSON.parse(readFileSync(configPath,'utf8'));}catch{return {};}}
28
+ function readConfig(){try{return JSON.parse(readFileSync(configPath(),'utf8'));}catch{return {};}}
24
29
  function writePrivate(path,text){
25
30
  if(existsSync(path))path=realpathSync(path); // a symlinked config stays a symlink
26
31
  mkdirSync(dirname(path),{recursive:true,mode:0o700});
@@ -37,9 +42,15 @@ function storedSecret(meta){
37
42
  function decode(raw){try{const parsed=JSON.parse(raw);if(typeof parsed.access_token==='string')return parsed;}catch{}return {access_token:raw};}
38
43
  const claims=token=>{try{return JSON.parse(Buffer.from(token.split('.')[0],'base64url').toString('utf8'))??{};}catch{return {};}};
39
44
  const expiresSoon=token=>claims(token).exp<=Date.now()/1000+60;
45
+ // Without a default keychain (a sudo or remote shell, a fresh account) the
46
+ // keychain write opens a macOS "A keychain cannot be found" dialog instead of
47
+ // failing, so the file is used without asking.
48
+ function defaultKeychain(){
49
+ try{const path=execFileSync('security',['default-keychain'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim().replace(/^"|"$/g,'');return path!==''&&existsSync(path);}catch{return false;}
50
+ }
40
51
  function store(credentials){
41
52
  const secret=JSON.stringify(credentials);
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{}}
53
+ if(process.platform==='darwin'&&!process.env.CAVE_NO_KEYCHAIN&&defaultKeychain()){try{execFileSync('security',['add-generic-password','-U',...keychain,'-w',secret],{stdio:'ignore'});return 'keychain';}catch{}}
43
54
  writePrivate(credentialsPath,secret);return 'file';
44
55
  }
45
56
  // Refresh tokens rotate: the new pair goes back where caveman reads it, or the
@@ -58,7 +69,7 @@ async function refresh(meta,credentials,baseURL){
58
69
  const now=readConfig();
59
70
  if(now.tokenStore===meta.tokenStore&&!now.logoutPendingLocalCleanup&&decode(storedSecret(now)).refresh_token===credentials.refresh_token){
60
71
  const {token,...rest}=now;
61
- writePrivate(configPath,JSON.stringify({...rest,tokenStore:store(next)},null,2)+'\n');
72
+ writePrivate(configPath(),JSON.stringify({...rest,tokenStore:store(next)},null,2)+'\n');
62
73
  }
63
74
  return next;
64
75
  }catch{return credentials;}
@@ -113,7 +124,7 @@ async function login(args=[]){
113
124
  // No gatewayUrl: with one, `caveman enable claude` switches to managed mode
114
125
  // and writes a gateway base URL with no key into Claude Code's settings.
115
126
  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');
127
+ writePrivate(configPath(),JSON.stringify({...rest,baseURL,tokenStore,...(typeof oid==='string'?{organizationId:oid}:{}),...(credentials.project_id?{projectId:credentials.project_id}:{})},null,2)+'\n');
117
128
  // The receipt: the server replays a durable grant until told it was stored,
118
129
  // and may revoke one that never is.
119
130
  if(Object.keys(credentials).length>1){
@@ -156,13 +167,13 @@ async function logout(){
156
167
  }
157
168
  // Remote revocation is recorded before the only local credential goes, so a
158
169
  // failed cleanup is retried without presenting a revoked session.
159
- writePrivate(configPath,JSON.stringify({...meta,logoutPendingLocalCleanup:true},null,2)+'\n');
170
+ writePrivate(configPath(),JSON.stringify({...meta,logoutPendingLocalCleanup:true},null,2)+'\n');
160
171
  if(meta.tokenStore==='keychain'||!meta.tokenStore&&process.platform==='darwin'&&!process.env.CAVE_NO_KEYCHAIN){
161
172
  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
173
  }
163
174
  if(meta.tokenStore!=='keychain')rmSync(credentialsPath,{force:true});
164
175
  const {token,tokenStore,projectId,organizationId,gatewayUrl,logoutPendingLocalCleanup,...rest}=meta;
165
- writePrivate(configPath,JSON.stringify({...rest,baseURL:''},null,2)+'\n');
176
+ writePrivate(configPath(),JSON.stringify({...rest,baseURL:''},null,2)+'\n');
166
177
  process.stdout.write(process.stdout.isTTY?'Signed out.\n':JSON.stringify({logged_out:true})+'\n');
167
178
  return 0;
168
179
  }
@@ -45,7 +45,7 @@ cvm auth status # who you are signed in as; exit 3 means signed out
45
45
  means this `cvm` and the server disagree: tell the user to install the `cvm`
46
46
  that matches their Caveman release (`npm i -g "@caveman-ai/cloud@VERSION"`).
47
47
  - `cvm` missing altogether: the user runs
48
- `npm i -g "@caveman-ai/cloud@>=0.1.0-rc.22"`, then `cvm setup`.
48
+ `npm i -g @caveman-ai/cloud@rc`, then `cvm setup`.
49
49
 
50
50
  ## 2. Find the operation, read its schema, call it
51
51
 
@@ -16,7 +16,7 @@ experiments; it does not route the agent's own LLM traffic.
16
16
  1. Check first: `cvm context show` and `cvm auth status`. A signed-in `cvm` on
17
17
  the task's project is a working connection; reuse it. Never select the first
18
18
  available project or silently substitute another identity.
19
- 2. If `cvm` is missing, the user runs `npm i -g "@caveman-ai/cloud@>=0.1.0-rc.22"`, then
19
+ 2. If `cvm` is missing, the user runs `npm i -g @caveman-ai/cloud@rc`, then
20
20
  `cvm setup --project PROJECT_ID` with the project from the dashboard's
21
21
  **Connect your coding agent** step. On a private install `CAVE_API_URL` names
22
22
  its API first. `cvm setup` signs in through the browser, saves the project
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caveman-ai/cloud",
3
- "version": "0.1.0-rc.25",
3
+ "version": "0.1.0-rc.26",
4
4
  "description": "cvm, the Caveman Cloud CLI: every Cloud operation from the terminal, plus the Cloud MCP server. Sign in with `cvm login`; `cvm setup` signs in, picks the project and installs the Caveman Cloud skill.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",