You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@every-env/spiral-cli

Package Overview
Dependencies
Maintainers
4
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@every-env/spiral-cli - npm Package Compare versions

Comparing version
0.1.0
to
0.2.0
+1
-1
package.json
{
"name": "@every-env/spiral-cli",
"version": "0.1.0",
"version": "0.2.0",
"description": "CLI for Spiral API - create content from your terminal",

@@ -5,0 +5,0 @@ "author": "Kieran Klaassen",

@@ -9,11 +9,27 @@ # spiral-cli

- macOS (Safari cookie extraction)
- macOS (Safari/Chrome/Firefox cookie extraction)
- [Bun](https://bun.sh/) >= 1.1.0
- Full Disk Access for terminal (macOS Sonoma+)
### Install from npm (recommended)
```bash
# Install globally
bun add -g @every-env/spiral-cli
# Or run directly without installing
bunx @every-env/spiral-cli chat "Write a tweet about AI"
```
After installation, the `spiral` command is available globally:
```bash
spiral chat "Your prompt here"
```
### Install from source
```bash
git clone <repo>
cd spiral-cli
git clone https://github.com/EveryInc/spiral-next.git
cd spiral-next/spiral-cli
bun install

@@ -20,0 +36,0 @@ ```

import { getCookies } from "@steipete/sweet-cookie";
import { config } from "./config";
import { AuthenticationError } from "./types";

@@ -6,2 +7,3 @@

const SPIRAL_URL = `https://${SPIRAL_DOMAIN}`;
const PAT_PREFIX = "spiral_sk_";

@@ -96,3 +98,76 @@ // Supported browsers for cookie extraction

/**
* Get valid auth token (from env, cache, or browser)
* Check if a token is a PAT (Personal Access Token)
*/
function isPAT(token: string): boolean {
return token.startsWith(PAT_PREFIX);
}
/**
* Get stored PAT from config
*/
export function getStoredPAT(): string | null {
const auth = config.get("auth");
return auth?.token || null;
}
/**
* Store PAT in config
*/
export function storePAT(token: string): void {
if (!isPAT(token)) {
throw new AuthenticationError(
`Invalid API key format. Keys should start with "${PAT_PREFIX}"`,
);
}
config.set("auth", {
token,
tokenPrefix: `${token.substring(0, 16)}...`,
createdAt: new Date().toISOString(),
});
// Clear any cached JWT
clearTokenCache();
}
/**
* Clear stored PAT
*/
export function clearStoredPAT(): void {
config.delete("auth");
clearTokenCache();
}
/**
* Get auth status info
*/
export function getAuthStatus(): {
method: "pat" | "browser" | "env" | "none";
tokenPrefix?: string;
createdAt?: string;
} {
// Check env first
if (process.env.SPIRAL_TOKEN) {
const token = process.env.SPIRAL_TOKEN;
return {
method: "env",
tokenPrefix: isPAT(token) ? `${token.substring(0, 16)}...` : "JWT token",
};
}
// Check stored PAT
const auth = config.get("auth");
if (auth?.token) {
return {
method: "pat",
tokenPrefix: auth.tokenPrefix,
createdAt: auth.createdAt,
};
}
return { method: "none" };
}
/**
* Get valid auth token (from env, stored PAT, cache, or browser)
* @security Uses in-memory cache, re-extracts on expiry

@@ -110,3 +185,12 @@ */

// 1. Check in-memory cache first (0ms vs 50-200ms disk access)
// 1. Check for stored PAT (long-lived, doesn't expire)
const storedPAT = getStoredPAT();
if (storedPAT) {
if (process.env.DEBUG) {
console.debug("Using stored PAT from config");
}
return storedPAT;
}
// 2. Check in-memory cache for JWT (0ms vs 50-200ms disk access)
if (tokenCache && !isTokenExpired(tokenCache.token)) {

@@ -116,16 +200,17 @@ return tokenCache.token;

// 2. Extract fresh token from browser cookies
// 3. Extract fresh JWT token from browser cookies
const token = await extractSpiralAuth();
// 3. Check if token is expired
// 4. Check if JWT token is expired
if (isTokenExpired(token)) {
throw new AuthenticationError(
"Session token has expired.\n\n" +
"To refresh: Open https://app.writewithspiral.com in your browser and refresh the page.\n" +
"The CLI will automatically pick up the fresh token.\n\n" +
"Tip: Keep a Spiral tab open while using the CLI for seamless token refresh.",
"To fix this, either:\n" +
" 1. Run `spiral auth login` and enter your API key\n" +
" 2. Open https://app.writewithspiral.com in your browser and refresh\n\n" +
"Get an API key at: https://app.writewithspiral.com → Account → API Keys",
);
}
// 4. Cache for future calls in this process
// 5. Cache for future calls in this process
tokenCache = { token, expiresAt: getTokenExpiry(token) };

@@ -132,0 +217,0 @@ return token;

@@ -5,2 +5,3 @@ #!/usr/bin/env bun

import { parseArgs } from "node:util";
import { password } from "@inquirer/prompts";
import chalk from "chalk";

@@ -12,3 +13,10 @@ import { marked } from "marked";

import { formatAttachmentsSummary, processAttachments } from "./attachments";
import { clearTokenCache, getAuthToken, sanitizeError } from "./auth";
import {
clearStoredPAT,
clearTokenCache,
getAuthStatus,
getAuthToken,
sanitizeError,
storePAT,
} from "./auth";
import { config } from "./config";

@@ -811,28 +819,27 @@ import {

switch (action) {
case "status":
case "login": {
console.log(theme.heading("\nSpiral CLI Login\n"));
console.log(
theme.dim("Get your API key at: https://app.writewithspiral.com → Account → API Keys\n"),
);
try {
const token = await getAuthToken();
if (values.json) {
console.log(
JSON.stringify({
status: "authenticated",
token_preview: `${token.slice(0, 20)}...`,
}),
);
} else {
console.log(theme.success("Authenticated"));
if (process.env.DEBUG) {
console.log(theme.dim(`Token: ${token.slice(0, 20)}...`));
}
const apiKey = await password({
message: "Enter your API key:",
mask: "•",
});
if (!apiKey || !apiKey.trim()) {
console.log(theme.error("No API key provided"));
process.exit(EXIT_CODES.AUTH_ERROR);
}
storePAT(apiKey.trim());
console.log(theme.success("\n✓ Logged in successfully!"));
console.log(theme.dim("Your API key has been saved securely.\n"));
} catch (error) {
if (values.json) {
console.log(
JSON.stringify({
status: "unauthenticated",
error: (error as Error).message,
}),
);
if ((error as Error).message?.includes("spiral_sk_")) {
console.log(theme.error((error as Error).message));
} else {
console.log(theme.error(`Not authenticated: ${(error as Error).message}`));
console.log(theme.error("Login cancelled"));
}

@@ -842,13 +849,71 @@ process.exit(EXIT_CODES.AUTH_ERROR);

break;
}
case "logout": {
const status = getAuthStatus();
if (status.method === "pat") {
clearStoredPAT();
console.log(theme.success("Logged out. API key removed."));
} else if (status.method === "env") {
console.log(
theme.warning("Using SPIRAL_TOKEN environment variable. Unset it to log out."),
);
} else {
console.log(theme.info("Not logged in with an API key."));
}
break;
}
case "status": {
const status = getAuthStatus();
if (values.json) {
console.log(JSON.stringify(status));
return;
}
switch (status.method) {
case "env":
console.log(theme.success("Authenticated via SPIRAL_TOKEN environment variable"));
console.log(theme.dim(`Token: ${status.tokenPrefix}`));
break;
case "pat":
console.log(theme.success("Authenticated with API key"));
console.log(theme.dim(`Key: ${status.tokenPrefix}`));
if (status.createdAt) {
console.log(theme.dim(`Saved: ${new Date(status.createdAt).toLocaleDateString()}`));
}
break;
case "none":
// Try browser fallback
try {
const token = await getAuthToken();
console.log(theme.success("Authenticated via browser session"));
console.log(theme.dim(`Token: ${token.slice(0, 20)}...`));
} catch {
console.log(theme.warning("Not authenticated"));
console.log(
theme.dim("\nRun `spiral auth login` to authenticate with an API key."),
);
}
break;
}
break;
}
case "clear":
clearStoredPAT();
clearTokenCache();
console.log(
theme.info("Token cache cleared. Re-login by visiting https://app.writewithspiral.com"),
);
console.log(theme.info("All credentials cleared."));
break;
default:
console.log(`
${theme.heading("Auth Commands:")}
spiral auth login Login with API key (recommended)
spiral auth logout Remove stored API key
spiral auth status Check authentication status
spiral auth clear Clear cached token
spiral auth clear Clear all stored credentials
${theme.dim("Get your API key at: https://app.writewithspiral.com → Account → API Keys")}
`);

@@ -870,3 +935,4 @@ }

spiral history <session-id> [--json] View session history
spiral auth [status|clear] Manage authentication
spiral auth login Login with API key
spiral auth [status|logout|clear] Manage authentication

@@ -873,0 +939,0 @@ ${theme.heading("Content Management:")}

@@ -28,2 +28,9 @@ // Local configuration store using `conf` package

// Auth credentials
export interface AuthCredentials {
token: string; // Personal Access Token (PAT)
tokenPrefix: string; // For display (spiral_sk_abc...)
createdAt: string;
}
// Main config schema

@@ -37,2 +44,3 @@ export interface SpiralConfig {

preferences: SpiralPreferences;
auth?: AuthCredentials;
}

@@ -39,0 +47,0 @@