Sign In

tashan-cli

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

tashan-cli - npm Package Compare versions

Comparing version
0.1.1
to
0.1.2
+54
-5
mcp.mjs

@@ -24,3 +24,3 @@ #!/usr/bin/env node

import { realpathSync } from "node:fs";
import { disp, search, find, installSnippets, pretty, slugify } from "./tashan.mjs";
import { disp, search, find, installSnippets, pretty, slugify, advisoriesOf, installScriptOf } from "./tashan.mjs";
import { tokensOf } from "./doctor.mjs";

@@ -79,4 +79,13 @@ import { configLocations, skillLocations, collect, resolve, assess, summarize } from "./doctor.mjs";

out.push("listed in OSV's malicious-packages database — this package IS the attack. Do not install it. Tell the user plainly and stop.");
else if (c.sec_advisory_count)
out.push(`${c.sec_advisory_count} known advisor${c.sec_advisory_count === 1 ? "y" : "ies"} against the current release (${(c.sec_max_severity || "unrated").toLowerCase()}) — see the capability page for which`);
else if (c.sec_advisory_count) {
// NAME THEM WHEN WE HAVE THEM. This line used to end "see the capability page for which" on every
// surface — the paywall speaking, since the detail was stripped from everything the CLI could
// read and pointing elsewhere was the only honest option left. sec_advisories is public now, so
// the ids go in the risk line itself; find_capability has no detail block under it, and an agent
// that can quote an id can act. The pointer stays as the fallback for the slim board record,
// which still carries the count but not the advisories.
const ids = advisoriesOf(c).map((a) => a.id).filter(Boolean);
out.push(`${c.sec_advisory_count} known advisor${c.sec_advisory_count === 1 ? "y" : "ies"} against the current release (${(c.sec_max_severity || "unrated").toLowerCase()})`
+ (ids.length ? ` — ${ids.join(", ")}` : " — see the capability page for which"));
}
if (c.sec_install_script)

@@ -299,2 +308,25 @@ out.push("runs a script at install time — arbitrary code executes on npm install, before any tool is called");

}
// THE DETAIL ITSELF, NOT A DESCRIPTION OF WHERE IT IS KEPT.
// This block used to say "which advisory and the version that fixes it" and "the exact command it
// runs at install time" were behind a Pro licence, then hand the agent a licensed URL to fetch —
// while the website printed both to any visitor. So the agent was told to warn and forbidden to
// act, over facts a browser gives away, and a model told a fix exists but not what it is will
// either hedge or invent one. The pipeline stopped redacting these (build.py's redact_paid), so
// they arrive in the public lookup.json, and the agent now gets them the way a reader does.
const advs = advisoriesOf(c);
const script = installScriptOf(c);
if (advs.length) {
L.push("", "Advisories against the version you would install today:");
for (const a of advs) {
L.push(` ${a.id} ${(a.severity || "unrated").toLowerCase()}${a.summary ? ` — ${a.summary}` : ""}`);
// The fixing version is the only actionable line here. "No fix published" has to be said out
// loud rather than omitted, or the agent reads silence as "already fixed, install away".
L.push(a.fixed ? ` fixed in ${a.fixed} — pin at or above it, or pick another capability`
: " no fixed version published — treat the current release as affected");
}
}
if (script) {
L.push("", `Runs this at install time, before any tool is called: ${script}`);
L.push("Show the user the command itself if they are deciding whether to install.");
}
L.push("", `Details: ${SITE}/capability/${c.slug || slugify(c.id)}`);

@@ -312,4 +344,21 @@ L.push("A tashan score measures adoption and maintenance, not security. We do not read its code.");

if (name === "check_capability") {
const all = await rows();
return renderCheck(find(all, String(args.name || "")), args.name);
// THE BOARD IS THE RANKING, NOT THE RECORD, and checking one named capability is a lookup
// question, not a ranking question. index.json carries 1,592 ranked rows, slimmed for the
// website's first paint: no sec_advisories, no sec_max_severity, no sec_permissions, and
// sec_install_script flattened to a bare 1. lookup.json carries all 5,951, in full.
//
// Checking the board alone was wrong in both directions. Every row we know an advisory about is
// OFF the board — low scores never rank, and the two dependency-confusion canaries are dropped
// from it on purpose — so an agent asking about mcp-server-taskwarrior, which has a command
// injection advisory, was told "not in the tashan index, unmeasured, not necessarily bad". And
// for the rows that did match, the slim record had none of the detail to relay.
//
// So: match the board (find() does fuzzy name/package matching, which the keyed lookup cannot),
// overlay the full public record, and fall back to the lookup when the board has never heard of
// it. Same lookup.json find_capability and audit_config already fetch, same cache, no licence.
const q = String(args.name || "");
const [all, lk] = [await rows(), await lookup().catch(() => null)];
const c = find(all, q);
const rec = lk ? resolve({ id: c ? c.id : q }, lk) : null;
return renderCheck(c || rec ? { ...(c || {}), ...(rec || {}) } : null, q);
}

@@ -316,0 +365,0 @@ if (name === "audit_config") {

+1
-1
{
"name": "tashan-cli",
"version": "0.1.1",
"version": "0.1.2",
"description": "The measured layer for AI capabilities — search, rank, and install MCP servers & agent skills from your terminal.",

@@ -5,0 +5,0 @@ "type": "module",

+167
-40

@@ -9,3 +9,5 @@ #!/usr/bin/env node

// npx tashan-cli doctor audit the config you actually have — dead, deprecated, risky
// npx tashan-cli activate <key> store your Pro licence on this machine (once, not per shell)
// npx tashan-cli login sign in — prints a code, opens the browser, approve, done
// npx tashan-cli logout release this machine's seat
// npx tashan-cli activate <key> non-interactive sign-in for CI, where there is no browser
// npx tashan-cli account open your account in the browser, already signed in

@@ -101,2 +103,94 @@ // npx tashan-cli mcp run as an MCP server, so your AGENT can ask before installing

// ---- device-authorisation login --------------------------------------------------------------
// `tashan activate <key>` asked every customer to go to their email, find a licence key, and paste
// it into a terminal — once per machine, forever. That is the worst moment in the product and it
// lands immediately after someone pays. This is the flow they already know from `gh auth login`,
// `wrangler login` and `stripe login`: ask for a code, approve it in a browser, the terminal picks
// up the credential. The key is typed at most once in a lifetime, in a browser, where paste is one
// keystroke. `activate` stays for scripts and CI, where a browser is not a thing.
export async function deviceLogin({ open = true, fetchImpl = fetch, sleep = (ms) => new Promise(r => setTimeout(r, ms)) } = {}) {
const post = async (body) => {
try {
const r = await fetchImpl(SITE + "/api/device", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body || {}),
});
return { status: r.status, json: await r.json().catch(() => null) };
} catch {
return { status: 0, json: null };
}
};
const started = await post({});
if (started.status !== 200 || !started.json || !started.json.device_code) {
process.stderr.write("\n " + red("Could not start sign-in.") +
dim("\n " + (started.status === 503 ? "Sign-in is temporarily unavailable." : "Check your connection.") +
"\n You can still use `tashan activate <key>` with the key from your purchase email.\n"));
return 1;
}
const { device_code, user_code, verify_url_complete, verify_url, interval, expires_in } = started.json;
const url = verify_url_complete || verify_url;
process.stdout.write("\n " + bold("Your code: ") + jade(user_code) + "\n" +
dim(" Approve it at " + url) + "\n");
if (open && openBrowser(url)) process.stdout.write(dim(" (opened in your browser)") + "\n");
process.stdout.write(dim("\n waiting…") + "\n");
const deadline = Date.now() + (Number(expires_in) || 600) * 1000;
let wait = Math.max(2, Number(interval) || 3) * 1000;
while (Date.now() < deadline) {
await sleep(wait);
const r = await post({ device_code });
const st = r.json && r.json.status;
if (st === "slow_down") { wait += 1000; continue; } // RFC 8628 §3.5 — back off, do not give up
if (st === "pending" || r.status === 0) continue; // offline blips are not a denial
if (st === "denied") {
process.stderr.write("\n " + red("Denied in the browser.") + dim("\n Nothing was stored.\n"));
return 1;
}
if (st === "ok" && r.json.key) return storeKey(r.json.key);
break;
}
process.stderr.write("\n " + red("That code expired.") + dim("\n Run `tashan login` again.\n"));
return 1;
}
// Register with Polar, then write the file — the same order `activate` uses, and for the same
// reason: Polar owns the device limit, so a key stored locally that Polar never activated looks
// fine here and fails everywhere else.
async function storeKey(key) {
const label = `${hostname()} \u00b7 ${platform()}`;
const { status, json } = await polar("/activate", { key, label });
if (status === 0) {
process.stderr.write("\n " + red("Signed in, but could not reach Polar to register this machine.") +
dim("\n Nothing was saved. Try again when you are online.\n"));
return 1;
}
if (status === 403) {
process.stderr.write("\n " + red("Every device slot on this licence is in use.") +
dim("\n Release one at " + PORTAL + ", or run `tashan logout` on a machine you no longer use.\n"));
return 1;
}
if (status !== 200 || !json || !json.id) {
process.stderr.write("\n " + red("The licence was rejected when registering this machine.") +
dim("\n Check your plan at " + PORTAL + "\n"));
return 1;
}
try {
mkdirSync(dirname(keyPath()), { recursive: true });
writeFileSync(keyPath(), JSON.stringify({ key, activation_id: json.id, label }, null, 2) + "\n", { mode: 0o600 });
chmodSync(keyPath(), 0o600);
} catch (e) {
await polar("/deactivate", { key, activation_id: json.id });
process.stderr.write("\n " + red("Could not write " + keyPath()) + dim("\n " + e.message + "\n"));
return 1;
}
process.stdout.write("\n " + jade("Pro is active on this machine.") +
dim(`\n registered as "${label}" \u2014 tashan account shows every device`) +
dim(`\n stored in ${keyPath()} \u00b7 every shell, every project, no re-export`) +
dim("\n\n tashan doctor") + "\n");
return 0;
}
const SITE = process.env.TASHAN_SITE || "https://tashan.sh";

@@ -122,2 +216,27 @@ const DATA_URL = SITE + "/data/index.json";

// ---- the security audit's DETAIL, read off the public row ---------------------------------------
// Both of these used to arrive from /api/security behind a licence header. build.py's redact_paid()
// no longer strips sec_advisories and no longer flattens sec_install_script to a boolean, so the
// detail rides in the public lookup.json the CLI already downloads. Free on the website, free here.
// Exported (and shared with mcp.mjs) so the terminal, the agent and the page cannot drift apart.
/** The advisories on a row: [{ id, severity, summary, fixed }]. Never throws — a malformed field
* must not take down an audit someone is running precisely because they are already worried. */
export function advisoriesOf(row) {
try {
const list = JSON.parse((row && row.sec_advisories) || "[]");
return Array.isArray(list) ? list : [];
} catch { return []; }
}
/** The literal command a package runs at install time, or null.
* THE BOARD DOES NOT CARRY IT. index.json slims sec_install_script to the number 1 (build.py's
* _slim) because the board only needs the FACT and the command can be 300 characters; only
* lookup.json has the string. A truthiness test would print "install runs: 1" on every board-sourced
* row — worse than silence — so this checks the type, not the truth. */
export function installScriptOf(row) {
const s = row && row.sec_install_script;
return typeof s === "string" && s.trim() && s !== "1" ? s : null;
}
export function search(rows, q) {

@@ -284,13 +403,11 @@ q = (q || "").toLowerCase().trim();

// The audit's paid half, on the surface that ADVERTISES it. doctor's free output says
// "N findings have detail behind a licence — which advisory and the version that fixes it,
// what the install script runs"; for a long time nothing printed any of that, on any surface.
// Fetching it here is what makes that sentence true in the place a customer reads it.
if (r.row.sec_advisory_count || r.row.sec_install_script) {
try {
const sr = await fetch(`${base}/api/security?id=${encodeURIComponent(r.row.id)}`,
{ headers: auth });
if (sr.ok) r.secDetail = await sr.json();
} catch { /* offline: the free finding is already printed and still correct */ }
}
// THE SECURITY AUDIT USED TO BE FETCHED HERE, from /api/security with the licence header, and it
// is gone on purpose. build.py's redact_paid() stopped stripping sec_advisories and stopped
// reducing sec_install_script to a boolean, so the advisory id, its severity, the version that
// fixes it and the literal install command now ship in the PUBLIC lookup.json — the same file
// doctor already downloads, and the same facts the website prints to anyone with a browser.
// Keeping the fetch meant the identical fact was free on the web and $6/mo in the terminal: one
// product contradicting itself depending on which surface you happened to be standing on.
// renderDoctor now reads the detail straight off `row`, with no key, no request and no branch.
// What stays behind this function is TIME — /api/history above — never the current state.
}

@@ -352,4 +469,6 @@ return results;

${jade("tashan doctor")} audit the config you already have — dead, deprecated, risky
${jade("tashan activate")} <key> register this machine — once, not once per shell
${jade("tashan login")} sign in — approve a code in the browser, once per machine
${jade("tashan logout")} release this machine's seat
${jade("tashan account")} open your account in the browser, already signed in
${dim("tashan activate <key>")} ${dim("non-interactive, for CI — needs the key")}
${dim("Pro names the replacement for anything dead in your config · $6/mo · " + SITE + "/pricing")}

@@ -398,3 +517,3 @@

if (!rows.length) out += " " + jade("+") + " " + dim("nothing deprecated, archived or abandoned.") + "\n";
for (const { item, row, assessment, alts, secDetail } of rows) {
for (const { item, row, assessment, alts } of rows) {
const t = row && row.tashan_score != null ? String(Math.round(row.tashan_score)) : "—";

@@ -407,18 +526,16 @@ out += " " + (MARK[assessment.level] || " ") + " " + bold(pretty(item.name).padEnd(28).slice(0, 28)) +

}
// The paid half, directly under the finding it explains. doctor's free output promises exactly
// this — "which advisory and the version that fixes it, what the install script runs" — and for
// a long time no surface printed any of it, which made the upsell a claim about nothing.
if (secDetail) {
for (const a of secDetail.advisories || []) {
out += " " + jade("→ ") + bold(a.id) +
dim(` ${(a.severity || "").toLowerCase()}`) +
(a.fixed ? dim(" · fixed in ") + jade(a.fixed) : dim(" · no fix published")) + "\n";
}
if (secDetail.install_script) {
out += " " + jade("→ ") + dim("install runs: ") + secDetail.install_script + "\n";
}
if ((secDetail.permissions || []).length > 1) {
out += " " + jade("→ ") + dim("can reach: ") + secDetail.permissions.join(", ") + "\n";
}
// WHICH advisory, and what the install script actually runs — directly under the finding that
// names it, for everyone, with no key and no request. This used to come from /api/security with
// a licence header while the website printed the identical facts to any visitor: the same
// product answering the same question two ways depending on the surface. Now it is read straight
// off `row`, which for doctor is the public lookup record.
// sec_permissions is deliberately NOT repeated here — assess() already emits "can reach: …" from
// the same row, free, and printing it twice would read as two separate findings.
for (const adv of advisoriesOf(row)) {
out += " " + jade("→ ") + bold(adv.id) +
dim(` ${(adv.severity || "").toLowerCase()}`) +
(adv.fixed ? dim(" · fixed in ") + jade(adv.fixed) : dim(" · no fix published")) + "\n";
}
const script = installScriptOf(row);
if (script) out += " " + jade("→ ") + dim("install runs: ") + script + "\n";
if (alts && alts.length) {

@@ -448,9 +565,13 @@ if (pro) {

if (!pro && keyState === null) {
const withDetail = rows.filter((r) => r.row &&
(r.row.sec_advisory_count || r.row.sec_install_script || (r.alts && r.alts.length))).length;
if (withDetail) {
out += "\n " + dim(`${withDetail} finding${withDetail === 1 ? " has" : "s have"} detail behind a licence — `) +
dim("which advisory and the version that fixes it, what the install script runs, ") +
dim("and the replacement to move to.") + "\n" +
" " + jade("tashan Pro") + dim(" $6/mo · " + SITE + "/pricing") + "\n";
// THE OFFER NAMES ONLY WHAT IS ACTUALLY WITHHELD. It used to count advisories and install scripts
// as well, and promise "which advisory and the version that fixes it" behind the licence — copy
// that is now false twice over: that detail is printed above, free, and pitching a reader
// something already on their screen is the fastest way to lose them. A licence buys the
// replacement to move to, and the history that says whether a thing is dying. Nothing else.
const replaceable = rows.filter((r) => r.row && r.alts && r.alts.length).length;
if (replaceable) {
out += "\n " + dim(`${replaceable} of these ${replaceable === 1 ? "has" : "have"} a measured replacement behind a licence — `) +
dim("the one to move to, and whether anything else here is on the way down.") + "\n" +
" " + jade("tashan Pro") + dim(" $6/mo · " + SITE + "/pricing") +
dim(" · already bought? ") + jade("tashan login") + "\n";
}

@@ -518,3 +639,9 @@ }

}
if (cmd === "activate") {
if (cmd === "login") return deviceLogin({ open: !a["no-browser"] });
// `logout` is what everyone types. It was only ever spelled `activate --forget`, which is a flag
// on a different verb — nobody guesses it, so nobody released a seat and device slots leaked.
if (cmd === "logout") a.forget = true;
if (cmd === "activate" || cmd === "logout") {
// --forget must RELEASE the seat at Polar, not just delete our file. Deleting locally while the

@@ -548,4 +675,4 @@ // activation stays registered burns a device slot the customer cannot get back without support.

: bold("This machine is not activated.") +
dim("\n tashan activate <key>") +
dim("\n Your key is in your purchase email · tashan account opens your plan and invoices")) + "\n");
dim("\n tashan login") + dim(" approve it in the browser — no key to copy") +
dim("\n tashan activate <key> if you are in CI and have no browser")) + "\n");
return lic ? 0 : 1;

@@ -599,3 +726,3 @@ }

process.stdout.write("\n " + bold("No licence on this machine.") +
dim("\n tashan activate <key> the key is in your purchase email") +
dim("\n tashan login sign in — no key to copy") +
dim("\n " + SITE + "/pricing what Pro adds · the index stays free") + "\n");

@@ -602,0 +729,0 @@ return 1;