@uplink-code/cli 0.0.1 → 0.1.0

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/lib/cli/bin.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ var P=Object.defineProperty;var o=(e,n)=>P(e,"name",{value:n,configurable:!0});import{Command as M}from"commander";import{createServer as N}from"node:http";import{randomBytes as R}from"node:crypto";import{exec as T}from"node:child_process";import{homedir as v}from"node:os";import{join as m}from"node:path";import{mkdirSync as x,readFileSync as O,writeFileSync as E,unlinkSync as I,existsSync as g,chmodSync as A}from"node:fs";function k(){let e=process.env.XDG_CONFIG_HOME;return e?m(e,"uplink"):m(v(),".config","uplink")}o(k,"credentialsDir");function d(){return m(k(),"credentials.json")}o(d,"credentialsFile");function y(){let e=d();if(!g(e))return null;let n=O(e,"utf-8"),t;try{t=JSON.parse(n)}catch{return null}return $(t)?t:null}o(y,"readCredentials");function _(e){let n=k();x(n,{recursive:!0,mode:448}),E(d(),JSON.stringify(e,null,2),{mode:384}),A(d(),384)}o(_,"writeCredentials");function w(){let e=d();return g(e)?(I(e),!0):!1}o(w,"clearCredentials");function l(){return d()}o(l,"credentialsPath");function $(e){if(e===null||typeof e!="object")return!1;let n=e;return typeof n.access_token=="string"&&typeof n.refresh_token=="string"&&typeof n.expires_at=="number"&&typeof n.organization_id=="string"&&typeof n.supabase_url=="string"&&typeof n.supabase_anon_key=="string"}o($,"isCredentials");var L="https://console.uplink.build",b=300*1e3;async function h(){let e=process.env.UPLINK_CONSOLE_URL??L,n=R(16).toString("hex"),{port:t,waitForCallback:s,shutdown:i}=await J({state:n}),r=`${e}/cli/login?port=${t}&state=${encodeURIComponent(n)}`;process.stdout.write(`Opening browser to sign in\u2026
3
+
4
+ If nothing opens, visit:
5
+ ${r}
6
+
7
+ `),z(r);try{let a=await s(),p={access_token:a.tokens.access_token,refresh_token:a.tokens.refresh_token,expires_at:a.tokens.expires_at,organization_id:a.organization_id,supabase_url:a.supabase_url,supabase_anon_key:a.supabase_anon_key};_(p),process.stdout.write(`\u2713 Signed in. Credentials saved to ${l()}
8
+ `)}finally{i()}}o(h,"login");function J(e){return new Promise((n,t)=>{let s,i,r=new Promise((c,f)=>{s=c,i=f}),a=N((c,f)=>{U(c,f,e.state,s,i)}),p=setTimeout(()=>{i(new Error(`Timed out after ${b/1e3}s waiting for sign-in. Re-run \`uplink login\`.`))},b);a.listen(0,"127.0.0.1",()=>{let c=a.address();n({port:c.port,waitForCallback:o(()=>r,"waitForCallback"),shutdown:o(()=>{clearTimeout(p),a.close()},"shutdown")})}),a.on("error",t)})}o(J,"startCallbackListener");async function U(e,n,t,s,i){if(n.setHeader("Access-Control-Allow-Origin","*"),n.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),n.setHeader("Access-Control-Allow-Headers","Content-Type"),e.method==="OPTIONS"){n.statusCode=204,n.end();return}if(e.method!=="POST"||e.url!=="/callback"){n.statusCode=404,n.end();return}try{let r=await F(e);if(!j(r))throw new Error("Malformed callback payload from console.");if(r.state!==t)throw new Error("State mismatch \u2014 refusing to accept tokens.");n.statusCode=200,n.end(JSON.stringify({ok:!0})),s({tokens:r.tokens,organization_id:r.organization_id,supabase_url:r.supabase_url,supabase_anon_key:r.supabase_anon_key})}catch(r){n.statusCode=400,n.end(JSON.stringify({ok:!1,error:r.message})),i(r instanceof Error?r:new Error(String(r)))}}o(U,"handleRequest");function F(e){return new Promise((n,t)=>{let s=[];e.on("data",i=>s.push(i)),e.on("end",()=>{try{n(JSON.parse(Buffer.concat(s).toString("utf-8")))}catch(i){t(i instanceof Error?i:new Error(String(i)))}}),e.on("error",t)})}o(F,"readJsonBody");function j(e){if(e===null||typeof e!="object")return!1;let n=e;if(typeof n.state!="string"||typeof n.organization_id!="string"||typeof n.supabase_url!="string"||typeof n.supabase_anon_key!="string")return!1;let t=n.tokens;if(t===null||typeof t!="object")return!1;let s=t;return typeof s.access_token=="string"&&typeof s.refresh_token=="string"&&typeof s.expires_at=="number"}o(j,"isCallbackPayload");function z(e){let n=process.platform==="darwin"?`open "${e}"`:process.platform==="win32"?`start "" "${e}"`:`xdg-open "${e}"`;T(n,t=>{})}o(z,"openInBrowser");function C(){w()?process.stdout.write(`\u2713 Removed ${l()}
9
+ `):process.stdout.write(`Already logged out.
10
+ `)}o(C,"logout");function S(){let e=y();e||(process.stdout.write("Not logged in. Run `uplink login`.\n"),process.exit(1));let n=H(e.access_token),t=n?.email??"(unknown)",s=n?.sub??"(unknown)",i=new Date(e.expires_at*1e3),r=e.expires_at*1e3<Date.now();process.stdout.write(`Email: ${t}
11
+ User id: ${s}
12
+ Expires: ${i.toISOString()}${r?" (expired \u2014 refresh on next call)":""}
13
+ Stored: ${l()}
14
+ `)}o(S,"whoami");function H(e){let n=e.split(".");if(n.length!==3)return null;try{let t=Buffer.from(n[1],"base64url").toString("utf-8");return JSON.parse(t)}catch{return null}}o(H,"decodeJwtPayload");var u=new M;u.name("uplink").description("CLI for the Uplink browser automation platform").version("0.0.1");u.command("login").description("Sign in via the browser and store credentials at ~/.config/uplink/credentials.json").action(()=>{h().catch(e=>{process.stderr.write(`Error: ${e.message}
15
+ `),process.exit(1)})});u.command("logout").description("Remove stored credentials").action(C);u.command("whoami").description("Show the currently signed-in user").action(S);u.parse();
package/lib/cli/index.js CHANGED
@@ -1,13 +1 @@
1
- #!/usr/bin/env node
2
- var C=Object.defineProperty;var i=(e,t)=>C(e,"name",{value:t,configurable:!0});import{Command as L}from"commander";import{homedir as K}from"node:os";import{join as l}from"node:path";import{mkdirSync as A,readFileSync as P,writeFileSync as S,unlinkSync as I,existsSync as d,chmodSync as b}from"node:fs";var c=l(K(),".uplink"),n=l(c,"credentials.json"),u="https://api.uplink.build";function m(){if(!d(n))return null;let e=P(n,"utf-8"),t=JSON.parse(e);if(t===null||typeof t!="object"||typeof t.apiKey!="string")return null;let p=t;return{apiKey:p.apiKey??"",apiUrl:p.apiUrl??u}}i(m,"readCredentials");function f(e){A(c,{recursive:!0,mode:448}),S(n,JSON.stringify(e,null,2),{mode:384}),b(n,384)}i(f,"writeCredentials");function y(){return d(n)?(I(n),!0):!1}i(y,"clearCredentials");function r(){return n}i(r,"credentialsPath");function g(){return u}i(g,"defaultApiUrl");function k(e){e.apiKey||(process.stderr.write(`Error: --api-key is required.
3
-
4
- Generate an API key in console (https://console.uplink.build \u2192 Settings \u2192 API Keys) and run:
5
- uplink login --api-key <key>
6
- `),process.exit(1)),f({apiKey:e.apiKey,apiUrl:e.apiUrl??g()}),process.stdout.write(`\u2713 Credentials written to ${r()}
7
- `)}i(k,"login");function w(){y()?process.stdout.write(`\u2713 Removed ${r()}
8
- `):process.stdout.write(`Already logged out.
9
- `)}i(w,"logout");function x(){let e=m();e||(process.stdout.write("Not logged in. Run `uplink login --api-key <key>`.\n"),process.exit(1));let t=e.apiKey.length>8?`${e.apiKey.slice(0,4)}\u2026${e.apiKey.slice(-4)}`:"****";process.stdout.write(`API key: ${t}
10
- API URL: ${e.apiUrl}
11
- Stored: ${r()}
12
- `)}i(x,"whoami");function s(e){process.stderr.write(`\`uplink session ${e}\` is not implemented in v0.0.1. Stay tuned.
13
- `),process.exit(1)}i(s,"stub");function v(){s("start")}i(v,"start");function h(){s("list")}i(h,"list");function U(){s("resume")}i(U,"resume");var o=new L;o.name("uplink").description("CLI for the Uplink browser automation platform").version("0.0.1");o.command("login").description("Store API credentials at ~/.uplink/credentials.json").requiredOption("--api-key <key>","API key generated in console").option("--api-url <url>","Override the Atomic API base URL").action(e=>{k(e)});o.command("logout").description("Remove stored credentials").action(w);o.command("whoami").description("Show stored credentials (masked)").action(x);var a=o.command("session").description("Manage Uplink sessions (stubbed in v0.0.1)");a.command("start").description("Create a new session").action(v);a.command("list").description("List recent sessions").action(h);a.command("resume <id>").description("Reattach to an existing session").action(U);o.parse();
1
+ var u=Object.defineProperty;var n=(e,r)=>u(e,"name",{value:r,configurable:!0});import{homedir as f}from"node:os";import{join as o}from"node:path";import{mkdirSync as d,readFileSync as _,writeFileSync as k,unlinkSync as y,existsSync as c,chmodSync as g}from"node:fs";function p(){let e=process.env.XDG_CONFIG_HOME;return e?o(e,"uplink"):o(f(),".config","uplink")}n(p,"credentialsDir");function s(){return o(p(),"credentials.json")}n(s,"credentialsFile");var h="https://api.uplink.build";function i(){let e=s();if(!c(e))return null;let r=_(e,"utf-8"),t;try{t=JSON.parse(r)}catch{return null}return b(t)?t:null}n(i,"readCredentials");function a(e){let r=p();d(r,{recursive:!0,mode:448}),k(s(),JSON.stringify(e,null,2),{mode:384}),g(s(),384)}n(a,"writeCredentials");function m(){let e=s();return c(e)?(y(e),!0):!1}n(m,"clearCredentials");function x(){return s()}n(x,"credentialsPath");function C(e){return process.env.UPLINK_API_HOST??e?.api_host??h}n(C,"resolveApiHost");function b(e){if(e===null||typeof e!="object")return!1;let r=e;return typeof r.access_token=="string"&&typeof r.refresh_token=="string"&&typeof r.expires_at=="number"&&typeof r.organization_id=="string"&&typeof r.supabase_url=="string"&&typeof r.supabase_anon_key=="string"}n(b,"isCredentials");async function w(){let e=i();if(!e)throw new Error("No stored credentials. Run `uplink login` from @uplink-code/cli first.");let r=await fetch(`${e.supabase_url}/auth/v1/token?grant_type=refresh_token`,{method:"POST",headers:{"Content-Type":"application/json",apikey:e.supabase_anon_key},body:JSON.stringify({refresh_token:e.refresh_token})});if(!r.ok)throw new Error(`Token refresh failed: ${r.status} ${r.statusText}. Re-run \`uplink login\`.`);let t=await r.json();if(typeof t.access_token!="string"||typeof t.refresh_token!="string"||typeof t.expires_at!="number")throw new Error("Token refresh response was malformed.");let l={...e,access_token:t.access_token,refresh_token:t.refresh_token,expires_at:t.expires_at};return a(l),l}n(w,"refreshAccessToken");export{m as clearCredentials,x as credentialsPath,i as readCredentials,w as refreshAccessToken,C as resolveApiHost,a as writeCredentials};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bin.d.ts.map
@@ -1,11 +1,10 @@
1
- export interface LoginOptions {
2
- apiKey?: string;
3
- apiUrl?: string;
4
- }
5
1
  /**
6
- * v0.0.1 supports paste-an-API-key only. Future: OAuth device flow
7
- * (open browser, receive callback, store token). Doing the simpler
8
- * thing first unblocks @uplink-code/mcp for developer-mode use.
2
+ * `uplink login` opens the browser to the console's `/cli/login`
3
+ * page, listens on an ephemeral localhost port for a POST containing
4
+ * the user's Supabase session, and writes the tokens to
5
+ * `~/.config/uplink/credentials.json`.
6
+ *
7
+ * Flow mirrors `gh auth login` / `gcloud auth login`.
9
8
  */
10
- export declare function login(opts: LoginOptions): void;
9
+ export declare function login(): Promise<void>;
11
10
  //# sourceMappingURL=login.d.ts.map
@@ -1,2 +1,8 @@
1
+ /**
2
+ * `uplink whoami` — decode the stored access_token (base64-url JSON, no
3
+ * signature verification — we trust the token because we stored it) and
4
+ * print identity + expiry. Returns non-zero exit if no credentials are
5
+ * stored.
6
+ */
1
7
  export declare function whoami(): void;
2
8
  //# sourceMappingURL=whoami.d.ts.map
@@ -1,10 +1,40 @@
1
+ /**
2
+ * Tokens + endpoints needed to talk to the Uplink API and to refresh
3
+ * the Supabase access token.
4
+ *
5
+ * Shape mirrors a subset of `@supabase/supabase-js`'s `Session` plus
6
+ * the public Supabase project URL + anon key (so MCP can refresh the
7
+ * access token without an additional config step). All values written
8
+ * here are non-secret apart from the tokens themselves; the anon key
9
+ * is the same public key console ships to every browser.
10
+ */
1
11
  export interface Credentials {
2
- apiKey: string;
3
- apiUrl: string;
12
+ access_token: string;
13
+ refresh_token: string;
14
+ /** Unix epoch seconds. */
15
+ expires_at: number;
16
+ /**
17
+ * Organization ID the user was operating under at login time. The
18
+ * `/sessions` POST needs this in its body because the token-auth
19
+ * middleware doesn't pick an org from the JWT alone; persisting at
20
+ * login time lets MCP send sessions without an extra round-trip.
21
+ */
22
+ organization_id: string;
23
+ /** Public Supabase project URL — used for token refresh. */
24
+ supabase_url: string;
25
+ /** Public Supabase anon key — used as `apikey` header on refresh. */
26
+ supabase_anon_key: string;
27
+ /** Optional override for the Uplink API host (defaults to prod). */
28
+ api_host?: string;
4
29
  }
5
30
  export declare function readCredentials(): Credentials | null;
6
31
  export declare function writeCredentials(creds: Credentials): void;
7
32
  export declare function clearCredentials(): boolean;
8
33
  export declare function credentialsPath(): string;
9
- export declare function defaultApiUrl(): string;
34
+ /**
35
+ * Resolve the API host the MCP should hit. Env override wins over the
36
+ * value persisted in the credentials file; both win over the prod
37
+ * default.
38
+ */
39
+ export declare function resolveApiHost(creds: Credentials | null): string;
10
40
  //# sourceMappingURL=credentials.d.ts.map
@@ -1,2 +1,9 @@
1
- export {};
1
+ /**
2
+ * Library entry — what other packages (notably @uplink-code/mcp) import
3
+ * from this CLI. Pure side-effect-free exports; the actual `uplink`
4
+ * binary lives in `./bin.ts`.
5
+ */
6
+ export { readCredentials, writeCredentials, clearCredentials, credentialsPath, resolveApiHost } from './credentials.ts';
7
+ export type { Credentials } from './credentials.ts';
8
+ export { refreshAccessToken } from './refresh.ts';
2
9
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,15 @@
1
+ import { type Credentials } from './credentials.ts';
2
+ /**
3
+ * Refresh the persisted access token via Supabase Auth's
4
+ * `/auth/v1/token?grant_type=refresh_token` endpoint, then write the
5
+ * new tokens back to the credentials file. Returns the refreshed
6
+ * `Credentials`.
7
+ *
8
+ * Callers (e.g. MCP) wrap this around any 401 response: catch, refresh,
9
+ * retry once.
10
+ *
11
+ * @throws if no credentials are stored, the refresh fails, or the
12
+ * response doesn't match the expected shape.
13
+ */
14
+ export declare function refreshAccessToken(): Promise<Credentials>;
15
+ //# sourceMappingURL=refresh.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uplink-code/cli",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -10,7 +10,7 @@
10
10
  }
11
11
  },
12
12
  "bin": {
13
- "uplink": "./lib/cli/index.js"
13
+ "uplink": "./lib/cli/bin.js"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public"
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "build": "npm run build:declarations && npm run build:transpile",
28
- "build:transpile": "esbuild src/index.ts --bundle --keep-names --minify --format=esm --platform=node --external:commander --banner:js='#!/usr/bin/env node' --outfile=./lib/cli/index.js",
28
+ "build:transpile": "esbuild src/index.ts --bundle --keep-names --minify --format=esm --platform=node --outfile=./lib/cli/index.js && esbuild src/bin.ts --bundle --keep-names --minify --format=esm --platform=node --external:commander --banner:js='#!/usr/bin/env node' --outfile=./lib/cli/bin.js",
29
29
  "build:declarations": "tsc -b",
30
30
  "clean": "rm -rf lib",
31
31
  "format:check": "prettier --check src/ package.json tsconfig.json",
@@ -1,14 +0,0 @@
1
- /**
2
- * Session commands — stubbed in v0.0.1.
3
- *
4
- * `start` will POST to the Atomic API to create a session and print
5
- * an OTP + QR for device pairing. `list` will fetch the user's
6
- * recent sessions. `resume` reattaches.
7
- *
8
- * Implementation lands once we have a shared Atomic API client
9
- * (likely `@uplink-code/api-client` in this same monorepo).
10
- */
11
- export declare function start(): void;
12
- export declare function list(): void;
13
- export declare function resume(): void;
14
- //# sourceMappingURL=session.d.ts.map