@askthew/mcp-plugin 0.4.10 → 0.4.12

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.
@@ -2,9 +2,9 @@ export declare function askTheWDataDir(env?: NodeJS.ProcessEnv): string;
2
2
  export declare function askTheWStateDir(env?: NodeJS.ProcessEnv): string;
3
3
  export declare function ensureAskTheWDataDir(env?: NodeJS.ProcessEnv): string;
4
4
  export declare function localStorePath(env?: NodeJS.ProcessEnv): string;
5
- export declare function identityPath(env?: NodeJS.ProcessEnv): string;
5
+ export declare function cloudTokenPath(env?: NodeJS.ProcessEnv): string;
6
+ export declare function installMetadataPath(env?: NodeJS.ProcessEnv): string;
6
7
  export declare function configPath(env?: NodeJS.ProcessEnv): string;
7
- export declare function jsonFallbackStorePath(env?: NodeJS.ProcessEnv): string;
8
8
  export declare function installReceiptsPath(env?: NodeJS.ProcessEnv): string;
9
9
  export declare function writePrivateJson(filePath: string, value: unknown): void;
10
10
  export declare function readJsonFile<T>(filePath: string): T | null;
package/dist/lib/paths.js CHANGED
@@ -29,17 +29,17 @@ export function ensureAskTheWDataDir(env = process.env) {
29
29
  return dir;
30
30
  }
31
31
  export function localStorePath(env = process.env) {
32
- return path.join(askTheWDataDir(env), "store.sqlite");
32
+ return path.join(askTheWDataDir(env), "outbox.sqlite");
33
33
  }
34
- export function identityPath(env = process.env) {
35
- return path.join(askTheWDataDir(env), "identity.json");
34
+ export function cloudTokenPath(env = process.env) {
35
+ return path.join(askTheWDataDir(env), "cloud-token.json");
36
+ }
37
+ export function installMetadataPath(env = process.env) {
38
+ return path.join(askTheWDataDir(env), "install.json");
36
39
  }
37
40
  export function configPath(env = process.env) {
38
41
  return path.join(askTheWDataDir(env), "config.json");
39
42
  }
40
- export function jsonFallbackStorePath(env = process.env) {
41
- return path.join(askTheWDataDir(env), "store.json");
42
- }
43
43
  export function installReceiptsPath(env = process.env) {
44
44
  return path.join(askTheWStateDir(env), "install-receipts.json");
45
45
  }
@@ -0,0 +1,32 @@
1
+ import type { AskTheWCloudClient } from "./cloud-client.js";
2
+ export type OutboxStatus = "pending" | "sent" | "failed";
3
+ export declare class Outbox {
4
+ private db;
5
+ constructor(path?: string);
6
+ append(payload: Record<string, unknown>): number;
7
+ pendingCount(): number;
8
+ markSent(id: number): void;
9
+ markFailed(id: number, code: string, message: string): void;
10
+ backoff(id: number, attempts: number, retryAfterSeconds?: number): number;
11
+ dueRows(limit?: number): Array<{
12
+ id: number;
13
+ payload_json: string;
14
+ attempts: number;
15
+ }>;
16
+ flush(client: AskTheWCloudClient): Promise<{
17
+ sent: number;
18
+ attempted: number;
19
+ }>;
20
+ }
21
+ export declare function isTransientStatus(status: number): boolean;
22
+ export declare function sendOutboxPayload(client: AskTheWCloudClient, payload: Record<string, unknown>, attempts?: number): Promise<{
23
+ kind: "sent";
24
+ body: unknown;
25
+ } | {
26
+ kind: "queued";
27
+ retryAfterSeconds?: number;
28
+ } | {
29
+ kind: "failed";
30
+ code: string;
31
+ message: string;
32
+ }>;
package/dist/outbox.js ADDED
@@ -0,0 +1,101 @@
1
+ import Database from "better-sqlite3";
2
+ import { localStorePath } from "./lib/paths.js";
3
+ const BACKOFF_SECONDS = [5, 15, 60, 300];
4
+ export class Outbox {
5
+ db;
6
+ constructor(path = localStorePath()) {
7
+ this.db = new Database(path);
8
+ this.db.pragma("journal_mode = WAL");
9
+ this.db.exec(`
10
+ create table if not exists outbox (
11
+ id integer primary key autoincrement,
12
+ scope_key text not null,
13
+ session_id text not null,
14
+ sequence integer not null,
15
+ payload_json text not null,
16
+ status text not null default 'pending',
17
+ attempts integer not null default 0,
18
+ next_attempt_at integer not null default 0,
19
+ error_code text,
20
+ error_message text,
21
+ created_at integer not null default (unixepoch())
22
+ );
23
+ create index if not exists outbox_pending_idx
24
+ on outbox(status, next_attempt_at, scope_key, session_id, sequence);
25
+ `);
26
+ }
27
+ append(payload) {
28
+ const result = this.db.prepare(`
29
+ insert into outbox(scope_key, session_id, sequence, payload_json, status, next_attempt_at)
30
+ values (?, ?, ?, ?, 'pending', 0)
31
+ `).run(String(payload.scopeKey ?? "global"), String(payload.sessionId ?? "session"), Number(payload.sequence ?? 0), JSON.stringify(payload));
32
+ return Number(result.lastInsertRowid);
33
+ }
34
+ pendingCount() {
35
+ return Number(this.db.prepare("select count(*) as count from outbox where status = 'pending'").get()?.count ?? 0);
36
+ }
37
+ markSent(id) {
38
+ this.db.prepare("update outbox set status = 'sent', error_code = null, error_message = null where id = ?").run(id);
39
+ }
40
+ markFailed(id, code, message) {
41
+ this.db.prepare("update outbox set status = 'failed', error_code = ?, error_message = ? where id = ?").run(code, message, id);
42
+ }
43
+ backoff(id, attempts, retryAfterSeconds) {
44
+ const backoff = Math.min(retryAfterSeconds ?? BACKOFF_SECONDS[Math.min(attempts, BACKOFF_SECONDS.length - 1)] ?? 300, 600);
45
+ const nextAttemptAt = Math.floor(Date.now() / 1000) + backoff;
46
+ this.db.prepare("update outbox set attempts = ?, next_attempt_at = ? where id = ?").run(attempts + 1, nextAttemptAt, id);
47
+ return backoff;
48
+ }
49
+ dueRows(limit = 25) {
50
+ const now = Math.floor(Date.now() / 1000);
51
+ return this.db.prepare(`
52
+ select id, payload_json, attempts from outbox
53
+ where status = 'pending' and next_attempt_at <= ?
54
+ order by scope_key asc, session_id asc, sequence asc, id asc
55
+ limit ?
56
+ `).all(now, limit);
57
+ }
58
+ async flush(client) {
59
+ const rows = this.dueRows();
60
+ let sent = 0;
61
+ for (const row of rows) {
62
+ const payload = JSON.parse(row.payload_json);
63
+ const result = await sendOutboxPayload(client, payload, row.attempts);
64
+ if (result.kind === "sent") {
65
+ this.markSent(row.id);
66
+ sent += 1;
67
+ }
68
+ else if (result.kind === "failed") {
69
+ this.markFailed(row.id, result.code, result.message);
70
+ }
71
+ else {
72
+ this.backoff(row.id, row.attempts, result.retryAfterSeconds);
73
+ }
74
+ }
75
+ return { sent, attempted: rows.length };
76
+ }
77
+ }
78
+ export function isTransientStatus(status) {
79
+ return status === 408 || status === 429 || status >= 500;
80
+ }
81
+ export async function sendOutboxPayload(client, payload, attempts = 0) {
82
+ try {
83
+ const response = await client.captureSignal(payload);
84
+ const body = response.body;
85
+ if (response.ok && body?.ok === true) {
86
+ return { kind: "sent", body };
87
+ }
88
+ if (isTransientStatus(response.status)) {
89
+ const retryAfter = Number((body?.retry_after_seconds ?? body?.retryAfterSeconds));
90
+ return { kind: "queued", retryAfterSeconds: Number.isFinite(retryAfter) ? retryAfter : undefined };
91
+ }
92
+ return {
93
+ kind: "failed",
94
+ code: String(body?.code ?? "hard_failure"),
95
+ message: String(body?.message ?? "Ask The W rejected the capture."),
96
+ };
97
+ }
98
+ catch {
99
+ return { kind: "queued", retryAfterSeconds: BACKOFF_SECONDS[Math.min(attempts, BACKOFF_SECONDS.length - 1)] };
100
+ }
101
+ }
@@ -0,0 +1,11 @@
1
+ export type RedactableSignalPayload = {
2
+ summary: string;
3
+ evidence?: Array<{
4
+ role: string;
5
+ excerpt: string;
6
+ }>;
7
+ filesTouched?: string[];
8
+ commandsRun?: string[];
9
+ metadata?: Record<string, unknown>;
10
+ };
11
+ export declare function redactSignalPayload<T extends RedactableSignalPayload>(payload: T): T;
@@ -0,0 +1,52 @@
1
+ const REDACTION_PATTERNS = [
2
+ /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/g,
3
+ /\bBearer\s+[A-Za-z0-9._-]+\b/g,
4
+ /\bghs_[A-Za-z0-9]{20,}\b/g,
5
+ /\bghp_[A-Za-z0-9]{20,}\b/g,
6
+ /\bsk-[A-Za-z0-9_-]{20,}\b/g,
7
+ /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g,
8
+ /\batw_(?:agent|agent_install)_[0-9a-f-]{36}_[A-Za-z0-9_-]{16,}\b/gi,
9
+ /\b\w{2,20}_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?:_[A-Za-z0-9]{8,})?\b/g,
10
+ ];
11
+ function redactText(value) {
12
+ return REDACTION_PATTERNS.reduce((text, pattern) => {
13
+ pattern.lastIndex = 0;
14
+ return text.replace(pattern, "[REDACTED]");
15
+ }, value);
16
+ }
17
+ function redactMetadata(value) {
18
+ if (typeof value === "string")
19
+ return redactText(value);
20
+ if (Array.isArray(value))
21
+ return value.map(redactMetadata);
22
+ if (typeof value === "object" && value !== null) {
23
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, redactMetadata(entry)]));
24
+ }
25
+ return value;
26
+ }
27
+ export function redactSignalPayload(payload) {
28
+ let redacted = false;
29
+ const redact = (value) => {
30
+ const next = redactText(value);
31
+ if (next !== value)
32
+ redacted = true;
33
+ return next;
34
+ };
35
+ const metadata = redactMetadata(payload.metadata ?? {});
36
+ if (JSON.stringify(metadata) !== JSON.stringify(payload.metadata ?? {}))
37
+ redacted = true;
38
+ return {
39
+ ...payload,
40
+ summary: redact(payload.summary),
41
+ evidence: (payload.evidence ?? []).map((entry) => ({
42
+ ...entry,
43
+ excerpt: redact(entry.excerpt),
44
+ })),
45
+ filesTouched: (payload.filesTouched ?? []).map(redact),
46
+ commandsRun: (payload.commandsRun ?? []).map(redact),
47
+ metadata: {
48
+ ...metadata,
49
+ ...(redacted ? { redacted: true } : {}),
50
+ },
51
+ };
52
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@askthew/mcp-plugin",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "private": false,
5
- "description": "Ask The W plugin connector for local-first coding-agent decisions, signals, and review.",
5
+ "description": "Ask The W cloud-shim MCP plugin for coding-agent decisions and signals.",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
@@ -1,28 +0,0 @@
1
- import type { LocalStore } from "./local-store.js";
2
- export declare function localScopeKey(cwd?: string): string;
3
- export declare function installPreCommitHook(input?: {
4
- cwd?: string;
5
- }): string;
6
- export declare function stagedFiles(input?: {
7
- cwd?: string;
8
- }): string[];
9
- export declare function preCommitDecisionGap(input: {
10
- store: LocalStore;
11
- stagedFiles: string[];
12
- now?: Date;
13
- scopeKey?: string | null;
14
- }): {
15
- missing: boolean;
16
- matchedSignals: number[];
17
- };
18
- export declare function isoWeek(date: Date): string;
19
- export declare function buildWeeklyDigest(input: {
20
- store: LocalStore;
21
- now?: Date;
22
- scopeKey?: string | null;
23
- }): string;
24
- export declare function writeWeeklyDigest(input: {
25
- store: LocalStore;
26
- now?: Date;
27
- outputDir?: string;
28
- }): string;
@@ -1,104 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { configPath, readJsonFile } from "./paths.js";
6
- import { resolvePluginScope } from "../scope.js";
7
- function readConfig() {
8
- return readJsonFile(configPath()) ?? {};
9
- }
10
- export function localScopeKey(cwd = process.cwd()) {
11
- const scope = resolvePluginScope(cwd);
12
- return [scope.repoRoot || scope.repoName || cwd, scope.appPath ?? "", scope.serviceName ?? ""]
13
- .filter(Boolean)
14
- .join("::")
15
- .replace(/\s+/g, " ")
16
- .slice(0, 500);
17
- }
18
- export function installPreCommitHook(input = {}) {
19
- const cwd = path.resolve(input.cwd ?? process.cwd());
20
- const gitDir = execFileSync("git", ["rev-parse", "--git-dir"], { cwd, encoding: "utf8" }).trim();
21
- const hookPath = path.resolve(cwd, gitDir, "hooks", "pre-commit");
22
- const hook = [
23
- "#!/bin/sh",
24
- "# Ask The W pre-commit decision prompt",
25
- "npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp hook-check --pre-commit",
26
- "",
27
- ].join("\n");
28
- fs.mkdirSync(path.dirname(hookPath), { recursive: true });
29
- fs.writeFileSync(hookPath, hook, { encoding: "utf8", mode: 0o755 });
30
- fs.chmodSync(hookPath, 0o755);
31
- return hookPath;
32
- }
33
- export function stagedFiles(input = {}) {
34
- const cwd = path.resolve(input.cwd ?? process.cwd());
35
- return execFileSync("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf8" })
36
- .split("\n")
37
- .map((line) => line.trim())
38
- .filter(Boolean);
39
- }
40
- export function preCommitDecisionGap(input) {
41
- const staged = new Set(input.stagedFiles);
42
- if (staged.size === 0) {
43
- return { missing: false, matchedSignals: [] };
44
- }
45
- const nowMs = (input.now ?? new Date()).getTime();
46
- const scopeKey = input.scopeKey;
47
- const recentImplementationSignals = input.store
48
- .listSignals({ scopeKey, limit: 100000 })
49
- .filter((signal) => signal.kind === "implementation_update")
50
- .filter((signal) => nowMs - new Date(signal.capturedAt).getTime() <= 14 * 24 * 60 * 60 * 1000)
51
- .filter((signal) => signal.filesTouched.some((file) => staged.has(file)));
52
- if (recentImplementationSignals.length === 0) {
53
- return { missing: false, matchedSignals: [] };
54
- }
55
- const matchedSignalIds = new Set(recentImplementationSignals.map((signal) => signal.id));
56
- const linkedDecision = input.store.listDecisions({ scopeKey, limit: 100000 }).some((decision) => {
57
- if (decision.sourceSignalIds.some((id) => matchedSignalIds.has(id)))
58
- return true;
59
- return decision.files.some((file) => staged.has(file));
60
- });
61
- return {
62
- missing: !linkedDecision,
63
- matchedSignals: Array.from(matchedSignalIds),
64
- };
65
- }
66
- export function isoWeek(date) {
67
- const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
68
- const day = utcDate.getUTCDay() || 7;
69
- utcDate.setUTCDate(utcDate.getUTCDate() + 4 - day);
70
- const yearStart = new Date(Date.UTC(utcDate.getUTCFullYear(), 0, 1));
71
- const week = Math.ceil(((utcDate.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
72
- return `${utcDate.getUTCFullYear()}-${String(week).padStart(2, "0")}`;
73
- }
74
- export function buildWeeklyDigest(input) {
75
- const now = input.now ?? new Date();
76
- const since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
77
- const scopeKey = input.scopeKey;
78
- const decisions = input.store.listDecisions({ scopeKey, since, limit: 100000 });
79
- const signals = input.store.listSignals({ scopeKey, limit: 100000 }).filter((signal) => signal.capturedAt >= since);
80
- const lines = [
81
- `# Ask The W Weekly Decision Digest ${isoWeek(now)}`,
82
- "",
83
- `Signals captured: ${signals.length}`,
84
- `Decisions captured: ${decisions.length}`,
85
- "",
86
- "## Decisions",
87
- ...(decisions.length
88
- ? decisions.map((decision) => `- ${decision.headline} (${decision.status})${decision.why ? ` - ${decision.why}` : ""}`)
89
- : ["- No decisions captured this week."]),
90
- ];
91
- if (readConfig().digest?.footer !== false) {
92
- lines.push("", "_Captured by Ask The W._");
93
- }
94
- return lines.join("\n");
95
- }
96
- export function writeWeeklyDigest(input) {
97
- const now = input.now ?? new Date();
98
- const configuredOutputDir = input.outputDir ?? process.env.ASKTHEW_DIGEST_DIR?.trim();
99
- const outputDir = configuredOutputDir || path.join(os.homedir(), "Documents");
100
- fs.mkdirSync(outputDir, { recursive: true });
101
- const filePath = path.join(outputDir, `askthew-digest-${isoWeek(now)}.md`);
102
- fs.writeFileSync(filePath, `${buildWeeklyDigest({ store: input.store, now, scopeKey: localScopeKey() })}\n`, "utf8");
103
- return filePath;
104
- }
@@ -1,27 +0,0 @@
1
- import { type LocalInstallIdentity, type PublicInstallIdentity } from "./local-identity.js";
2
- export interface FreeInstallRegistrationOptions {
3
- apiUrl?: string;
4
- fetchImpl?: typeof fetch;
5
- }
6
- export declare function registerFreeInstall(input: {
7
- identity: LocalInstallIdentity;
8
- deviceLabel?: string;
9
- repo?: Record<string, unknown>;
10
- options?: FreeInstallRegistrationOptions;
11
- }): Promise<{
12
- ok: boolean;
13
- registeredAt: string;
14
- }>;
15
- export declare function tryRegisterFreeInstall(input: {
16
- identity: LocalInstallIdentity;
17
- deviceLabel?: string;
18
- repo?: Record<string, unknown>;
19
- options?: FreeInstallRegistrationOptions;
20
- }): Promise<{
21
- ok: boolean;
22
- registeredAt: string;
23
- } | {
24
- ok: boolean;
25
- error: string;
26
- }>;
27
- export declare function describeFreeIdentity(identity: PublicInstallIdentity): string;
@@ -1,52 +0,0 @@
1
- import crypto from "node:crypto";
2
- import { markLocalIdentityRegistered, } from "./local-identity.js";
3
- function baseUrl(apiUrl) {
4
- return (apiUrl?.trim() || process.env.ASKTHEW_API_URL?.trim() || "https://app.askthew.com").replace(/\/$/, "");
5
- }
6
- function hashClaimCode(code) {
7
- return crypto.createHash("sha256").update(code.trim()).digest("hex");
8
- }
9
- function safeMessage(error) {
10
- return error instanceof Error ? error.message : "Registration failed.";
11
- }
12
- export async function registerFreeInstall(input) {
13
- const fetcher = input.options?.fetchImpl ?? fetch;
14
- const response = await fetcher(`${baseUrl(input.options?.apiUrl ?? input.identity.apiUrl)}/api/cli/v1/free-installs/register`, {
15
- method: "POST",
16
- headers: { "Content-Type": "application/json" },
17
- body: JSON.stringify({
18
- installId: input.identity.installId,
19
- publicKey: input.identity.publicKey,
20
- claimCodeHash: hashClaimCode(input.identity.claimCode),
21
- emailClaim: input.identity.emailClaim,
22
- deviceLabel: input.deviceLabel,
23
- repo: input.repo ?? {},
24
- }),
25
- });
26
- const payload = await response.json().catch(() => null);
27
- if (!response.ok) {
28
- throw new Error(payload?.error ? String(payload.error) : "Free install registration failed.");
29
- }
30
- const registeredAt = payload && typeof payload === "object" && typeof payload.registeredAt === "string"
31
- ? payload.registeredAt
32
- : new Date().toISOString();
33
- markLocalIdentityRegistered({ registeredAt });
34
- return { ok: true, registeredAt };
35
- }
36
- export async function tryRegisterFreeInstall(input) {
37
- try {
38
- return await registerFreeInstall(input);
39
- }
40
- catch (error) {
41
- markLocalIdentityRegistered({ registrationError: safeMessage(error) });
42
- return { ok: false, error: safeMessage(error) };
43
- }
44
- }
45
- export function describeFreeIdentity(identity) {
46
- return [
47
- `Install ID: ${identity.installId}`,
48
- identity.emailClaim ? `Email claim: ${identity.emailClaim} (unverified)` : "Email claim: none",
49
- `Claim code: ${identity.claimCode}`,
50
- identity.registeredAt ? `Registered: ${identity.registeredAt}` : "Registered: pending",
51
- ].join("\n");
52
- }
@@ -1,22 +0,0 @@
1
- import { type LocalInstallIdentity } from "./local-identity.js";
2
- export type McpMode = "paid" | "free" | "free_pending_auth" | "unauthenticated";
3
- export interface CliCredentials {
4
- email?: string;
5
- userId: string;
6
- cliToken: string;
7
- cliTokenId: string;
8
- apiUrl?: string;
9
- telemetryOptOut?: boolean;
10
- identityKind: "local_install";
11
- installId?: string;
12
- localIdentity?: LocalInstallIdentity;
13
- }
14
- export interface ModeResolution {
15
- mode: McpMode;
16
- installToken?: string;
17
- cliCredentials?: CliCredentials;
18
- reason: string;
19
- }
20
- export declare function loadCliCredentials(env?: NodeJS.ProcessEnv): CliCredentials | null;
21
- export declare function resolveMcpMode(env?: NodeJS.ProcessEnv): ModeResolution;
22
- export declare function isTelemetryOptedOut(env?: NodeJS.ProcessEnv, credentials?: CliCredentials | null): boolean;
@@ -1,52 +0,0 @@
1
- import { loadLocalIdentity } from "./local-identity.js";
2
- function clean(value) {
3
- return String(value ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
4
- }
5
- export function loadCliCredentials(env = process.env) {
6
- const localIdentity = loadLocalIdentity(env);
7
- if (localIdentity) {
8
- return {
9
- email: localIdentity.emailClaim,
10
- userId: localIdentity.installId,
11
- cliToken: localIdentity.installId,
12
- cliTokenId: localIdentity.installId,
13
- apiUrl: localIdentity.apiUrl,
14
- telemetryOptOut: localIdentity.telemetryOptOut,
15
- identityKind: "local_install",
16
- installId: localIdentity.installId,
17
- localIdentity,
18
- };
19
- }
20
- return null;
21
- }
22
- export function resolveMcpMode(env = process.env) {
23
- const installToken = clean(env.ASKTHEW_INSTALL_TOKEN);
24
- if (installToken) {
25
- return {
26
- mode: "paid",
27
- installToken,
28
- reason: "workspace_install_token",
29
- };
30
- }
31
- const credentials = loadCliCredentials(env);
32
- if (credentials?.cliToken) {
33
- return {
34
- mode: "free",
35
- cliCredentials: credentials,
36
- reason: "local_install_identity",
37
- };
38
- }
39
- if (clean(env.ASKTHEW_FREE_MODE) === "1" || clean(env.ASKTHEW_FREE_MODE).toLowerCase() === "true") {
40
- return {
41
- mode: "free_pending_auth",
42
- reason: "free_mode_no_identity",
43
- };
44
- }
45
- return {
46
- mode: "unauthenticated",
47
- reason: "no_identity",
48
- };
49
- }
50
- export function isTelemetryOptedOut(env = process.env, credentials = loadCliCredentials(env)) {
51
- return env.ASKTHEW_TELEMETRY === "off" || credentials?.telemetryOptOut === true;
52
- }
@@ -1,44 +0,0 @@
1
- export interface LocalInstallIdentity {
2
- installId: string;
3
- privateKey: string;
4
- publicKey: string;
5
- claimCode: string;
6
- emailClaim?: string;
7
- createdAt: string;
8
- registeredAt?: string;
9
- registrationError?: string;
10
- apiUrl?: string;
11
- telemetryOptOut?: boolean;
12
- }
13
- export interface PublicInstallIdentity {
14
- installId: string;
15
- publicKey: string;
16
- claimCode: string;
17
- emailClaim?: string;
18
- createdAt: string;
19
- registeredAt?: string;
20
- registrationError?: string;
21
- apiUrl?: string;
22
- telemetryOptOut?: boolean;
23
- }
24
- export declare function loadLocalIdentity(env?: NodeJS.ProcessEnv): LocalInstallIdentity | null;
25
- export declare function publicIdentity(identity: LocalInstallIdentity): PublicInstallIdentity;
26
- export declare function ensureLocalIdentity(input?: {
27
- emailClaim?: string | null;
28
- apiUrl?: string;
29
- telemetryOptOut?: boolean;
30
- env?: NodeJS.ProcessEnv;
31
- }): LocalInstallIdentity;
32
- export declare function markLocalIdentityRegistered(input: {
33
- registeredAt?: string;
34
- registrationError?: string;
35
- env?: NodeJS.ProcessEnv;
36
- }): LocalInstallIdentity | null;
37
- export declare function signLocalIdentityPayload(input: {
38
- identity: LocalInstallIdentity;
39
- body: string;
40
- timestamp?: string;
41
- }): {
42
- timestamp: string;
43
- signature: string;
44
- };
@@ -1,81 +0,0 @@
1
- import crypto from "node:crypto";
2
- import { identityPath, readJsonFile, writePrivateJson } from "./paths.js";
3
- function normalizeEmail(email) {
4
- const value = String(email ?? "").trim().toLowerCase();
5
- return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value) ? value : undefined;
6
- }
7
- function generateClaimCode() {
8
- return crypto.randomBytes(6).toString("base64url").toUpperCase();
9
- }
10
- export function loadLocalIdentity(env = process.env) {
11
- const identity = readJsonFile(identityPath(env));
12
- if (!identity?.installId || !identity.privateKey || !identity.publicKey || !identity.claimCode) {
13
- return null;
14
- }
15
- return identity;
16
- }
17
- export function publicIdentity(identity) {
18
- return {
19
- installId: identity.installId,
20
- publicKey: identity.publicKey,
21
- claimCode: identity.claimCode,
22
- emailClaim: identity.emailClaim,
23
- createdAt: identity.createdAt,
24
- registeredAt: identity.registeredAt,
25
- registrationError: identity.registrationError,
26
- apiUrl: identity.apiUrl,
27
- telemetryOptOut: identity.telemetryOptOut,
28
- };
29
- }
30
- export function ensureLocalIdentity(input = {}) {
31
- const env = input.env ?? process.env;
32
- const existing = loadLocalIdentity(env);
33
- const emailClaim = normalizeEmail(input.emailClaim);
34
- if (existing) {
35
- const next = {
36
- ...existing,
37
- ...(emailClaim ? { emailClaim } : {}),
38
- ...(input.apiUrl ? { apiUrl: input.apiUrl } : {}),
39
- ...(typeof input.telemetryOptOut === "boolean" ? { telemetryOptOut: input.telemetryOptOut } : {}),
40
- };
41
- writePrivateJson(identityPath(env), next);
42
- return next;
43
- }
44
- const keyPair = crypto.generateKeyPairSync("ed25519", {
45
- privateKeyEncoding: { type: "pkcs8", format: "pem" },
46
- publicKeyEncoding: { type: "spki", format: "pem" },
47
- });
48
- const identity = {
49
- installId: crypto.randomUUID(),
50
- privateKey: keyPair.privateKey,
51
- publicKey: keyPair.publicKey,
52
- claimCode: generateClaimCode(),
53
- ...(emailClaim ? { emailClaim } : {}),
54
- ...(input.apiUrl ? { apiUrl: input.apiUrl } : {}),
55
- ...(typeof input.telemetryOptOut === "boolean" ? { telemetryOptOut: input.telemetryOptOut } : {}),
56
- createdAt: new Date().toISOString(),
57
- };
58
- writePrivateJson(identityPath(env), identity);
59
- return identity;
60
- }
61
- export function markLocalIdentityRegistered(input) {
62
- const env = input.env ?? process.env;
63
- const identity = loadLocalIdentity(env);
64
- if (!identity)
65
- return null;
66
- const next = {
67
- ...identity,
68
- registeredAt: input.registrationError ? identity.registeredAt : (input.registeredAt ?? new Date().toISOString()),
69
- registrationError: input.registrationError,
70
- };
71
- writePrivateJson(identityPath(env), next);
72
- return next;
73
- }
74
- export function signLocalIdentityPayload(input) {
75
- const timestamp = input.timestamp ?? new Date().toISOString();
76
- const signature = crypto.sign(null, Buffer.from(`${timestamp}.${input.body}`), input.identity.privateKey).toString("base64url");
77
- return {
78
- timestamp,
79
- signature,
80
- };
81
- }