Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "axme-code",
"displayName": "AXME Code",
"description": "Persistent memory, decisions, and safety guardrails for Cursor, GitHub Copilot, Cline, Continue, Roo Code, Windsurf, and VS Code chat agents",
"version": "0.0.1",
"version": "0.0.2",
"publisher": "AxmeAI",
"repository": {
"type": "git",
Expand Down Expand Up @@ -80,6 +80,21 @@
"command": "axme.showStatus",
"title": "AXME: Show status",
"category": "AXME"
},
{
"command": "axme.showStatusText",
"title": "AXME: Show status (text output)",
"category": "AXME"
},
{
"command": "axme.reauthAuditor",
"title": "AXME: Reauth auditor (paste new API key)",
"category": "AXME"
},
{
"command": "axme.reset",
"title": "AXME: Reset (clear hooks + auth on this machine)",
"category": "AXME"
}
]
},
Expand Down
82 changes: 82 additions & 0 deletions extension/src/activation-report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Activation status report — tracks success/failure of each step during
* `activate()` so we can surface a clear summary at the end.
*
* Without this, a half-broken install (e.g. hooks failed to write, auditor
* auth never persisted) leaves the user with no visible signal that
* something is wrong — they only find out hours later when an action that
* should have been blocked goes through, or when audit doesn't run.
*
* The report is rendered as:
* - one-line summary toast when everything succeeded:
* "AXME Code ready: ✓ MCP ✓ Hooks ✓ Auth (claude) ✓ KB (33 dec, 6 mems)"
* - notification with [Show output] button when ANY step failed:
* "AXME Code: partial install — Hooks ✗ (see output)"
*
* Each step records (kind, ok, detail). The summary is built when
* activation completes.
*/

import * as vscode from "vscode";
import { show as showOutput } from "./log.js";

export type StepKind = "mcp" | "hooks" | "auth" | "setup" | "binary";

interface Step {
kind: StepKind;
ok: boolean;
detail: string;
}

const LABELS: Record<StepKind, string> = {
binary: "Binary",
mcp: "MCP",
hooks: "Hooks",
auth: "Auth",
setup: "KB",
};

export class ActivationReport {
private readonly steps: Step[] = [];

record(kind: StepKind, ok: boolean, detail: string): void {
this.steps.push({ kind, ok, detail });
}

/** Render success/failure summary as a single line. */
summary(): string {
return this.steps
.map((s) => `${s.ok ? "✓" : "✗"} ${LABELS[s.kind]}${s.detail ? ` (${s.detail})` : ""}`)
.join(" ");
}

hasFailure(): boolean {
return this.steps.some((s) => !s.ok);
}

failedSteps(): Step[] {
return this.steps.filter((s) => !s.ok);
}

/**
* Show the result to the user. Non-modal in both branches. Success path
* is dismissed automatically after VS Code's notification timeout; failure
* path has a "Show output" button so the user can inspect the AXME Code
* channel for the actual error.
*/
async present(): Promise<void> {
const text = this.summary();
if (!this.hasFailure()) {
// Success — short info notification.
void vscode.window.showInformationMessage(`AXME Code ready: ${text}`);
return;
}
const failedNames = this.failedSteps().map((s) => LABELS[s.kind]).join(", ");
const choice = await vscode.window.showWarningMessage(
`AXME Code: partial install — ${failedNames} ${this.failedSteps().length === 1 ? "failed" : "failed"}. ${text}`,
"Show output",
"Dismiss",
);
if (choice === "Show output") showOutput();
}
}
14 changes: 14 additions & 0 deletions extension/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { IdeKind } from "./ide-detect.js";
import { runSetup } from "./setup-controller.js";
import { ensureAuditorAuth } from "./auditor-auth.js";
import { AxmeStatusBar } from "./status-bar.js";
import { openStatusWebview } from "./status-webview.js";
import { runReset } from "./reset.js";
import { log, logError, show as showOutput } from "./log.js";

function workspaceRoot(): string | undefined {
Expand Down Expand Up @@ -67,6 +69,14 @@ export function registerCommands(
}),

vscode.commands.registerCommand("axme.showStatus", async () => {
// v0.0.2: replace plain-text output dump with a full healthcheck
// webview (status of binary, MCP, hooks, auth, KB per workspace).
// The old "axme-code status" output dump is still accessible via the
// axme.showStatusText fallback command for power users.
await openStatusWebview(binary);
}),

vscode.commands.registerCommand("axme.showStatusText", async () => {
const root = workspaceRoot();
if (!root) {
void vscode.window.showWarningMessage("AXME Code: open a folder first.");
Expand Down Expand Up @@ -116,5 +126,9 @@ export function registerCommands(
await vscode.window.showTextDocument(doc);
}
}),

vscode.commands.registerCommand("axme.reset", async () => {
await runReset();
}),
];
}
147 changes: 107 additions & 40 deletions extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* AXME Code — Cursor extension entry point (v0.0.1, Cursor-only).
* AXME Code — Cursor extension entry point (v0.0.2, Cursor-only).
*
* Activation flow:
* 1. Detect Cursor (vs VS Code or other fork). Bail out with a friendly
Expand All @@ -11,12 +11,17 @@
* 6. If the workspace is not initialised yet, offer to run `axme-code setup`.
* 7. Attach the AXME status bar and register commands.
*
* Every step's outcome is recorded in an ActivationReport; at the end we
* show a single summary toast with ✓/✗ per step so the user can see at a
* glance that everything is wired up (or which step needs attention).
* Without this, partial installs used to fail silently — the user only
* noticed hours later when a safety hook didn't fire or an audit didn't
* run.
*
* Deactivation disposes the MCP registration (Cursor unregisters the
* server), the status bar, the FS watcher, and all commands. User-level
* hooks at ~/.cursor/hooks.json are NOT removed on deactivate — the user
* can remove them manually if they uninstall the extension. (VS Code's
* deactivate fires on plain window-close too, so blanket-removing hooks
* there would be wrong.)
* can clear them via `AXME: Reset` command if uninstalling.
*/

import * as vscode from "vscode";
Expand All @@ -25,26 +30,59 @@ import { findAxmeBinary } from "./binary-detect.js";
import { registerMcpServer } from "./mcp-register.js";
import { installUserHooks } from "./hooks-install.js";
import { ensureAuditorAuth } from "./auditor-auth.js";
import { offerSetupIfMissing } from "./setup-controller.js";
import { offerSetupIfMissing, isAxmeInitialized } from "./setup-controller.js";
import { AxmeStatusBar } from "./status-bar.js";
import { registerCommands } from "./commands.js";
import { readCounts } from "./kb-watcher.js";
import { ActivationReport, StepKind } from "./activation-report.js";
import { log, logError, show as showOutput, dispose as disposeLog } from "./log.js";

declare const __EXTENSION_VERSION__: string;

let statusBar: AxmeStatusBar | undefined;

/**
* Run an activation step inside a try/catch. On failure: record the step
* as failed in the report AND surface a non-modal warning notification
* with a [Show output] button so the user is not left guessing. The
* activation flow continues past the failure — partial functionality is
* better than total failure (e.g. MCP registered but hooks failed → user
* still gets axme tools, just no machine-wide safety blocks).
*/
async function runStep<T>(
report: ActivationReport,
kind: StepKind,
successDetail: (result: T) => string,
body: () => Promise<T>,
): Promise<T | undefined> {
try {
const result = await body();
report.record(kind, true, successDetail(result));
return result;
} catch (err) {
logError(`Step ${kind}`, err);
report.record(kind, false, (err as Error).message.slice(0, 80));
void vscode.window
.showWarningMessage(
`AXME Code: ${kind} step failed — ${(err as Error).message}`,
"Show output",
)
.then((c) => { if (c === "Show output") showOutput(); });
return undefined;
}
}

export async function activate(context: vscode.ExtensionContext): Promise<void> {
log(`AXME Code v${__EXTENSION_VERSION__} activating…`);

// ---- Step 1: Cursor gate -------------------------------------------------
// ---- Step 1: Cursor gate -----------------------------------------------
const ide: IdeKind = detectIde();
log(` Host IDE: ${ide}`);
if (ide !== "cursor") {
log(" Not running in Cursor — extension will not register any tools.");
void vscode.window
.showWarningMessage(
"AXME Code v0.0.1 requires Cursor. VS Code / Copilot / Cline support is " +
"AXME Code v0.0.x requires Cursor. VS Code / Copilot / Cline support is " +
"on the roadmap once Microsoft adds chat-tool interception + chat-end " +
"lifecycle APIs.",
"Open output",
Expand All @@ -55,66 +93,95 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
return;
}

// ---- Step 2: binary -----------------------------------------------------
const binary = await findAxmeBinary(context);
const report = new ActivationReport();

// ---- Step 2: binary detection ------------------------------------------
const binary = await runStep(report, "binary", (b) => b.split("/").pop() ?? "ok", async () => {
const path = await findAxmeBinary(context);
if (!path) {
throw new Error(
"bundled axme-code binary not found inside this .vsix. " +
"Reinstall the extension; if the problem persists, file an issue.",
);
}
log(` Binary: ${path}`);
return path;
});
if (!binary) {
log(" axme-code binary not found.");
void vscode.window.showErrorMessage(
"AXME Code: bundled axme-code binary not found inside this .vsix. " +
"Please file an issue at github.com/AxmeAI/axme-code/issues. As a " +
"workaround, install axme-code separately and set `axme.binaryPath`.",
);
// Binary missing is fatal — every other step needs it. Show summary
// anyway so user sees the failure in toast form.
await report.present();
return;
}
log(` Binary: ${binary}`);

// ---- Step 3: MCP registration ------------------------------------------
try {
const mcpDisposable = await registerMcpServer(binary);
context.subscriptions.push(mcpDisposable);
} catch (err) {
logError("MCP register", err);
void vscode.window.showErrorMessage(
`AXME Code: MCP registration failed — ${(err as Error).message}. ` +
"See AXME Code output channel.",
);
// Continue activation so user can still see output + try Reauth / Setup.
}
await runStep(report, "mcp", () => "registered", async () => {
const disposable = await registerMcpServer(binary);
context.subscriptions.push(disposable);
});

// ---- Step 4: hooks ------------------------------------------------------
const enableHooks = vscode.workspace
.getConfiguration("axme")
.get<boolean>("enableHooks", true);
if (enableHooks) {
try {
installUserHooks("cursor", binary);
} catch (err) {
logError("Hooks install", err);
}
await runStep(report, "hooks", () => "user-level", async () => {
const ok = installUserHooks("cursor", binary);
if (!ok) throw new Error("hooks install returned false");
});
} else {
log("Hooks: disabled by axme.enableHooks setting");
report.record("hooks", true, "disabled by setting");
}

// ---- Step 5: auditor auth ----------------------------------------------
try {
await ensureAuditorAuth(binary);
} catch (err) {
logError("Auditor auth", err);
}
await runStep(report, "auth", (mode) => mode ?? "?", async () => {
const mode = await ensureAuditorAuth(binary);
return mode;
});

// ---- Step 6: setup offer -----------------------------------------------
void offerSetupIfMissing(binary, "cursor");
// ---- Step 6: setup offer (non-blocking, fire-and-forget) ---------------
// Setup is the user's job, not part of activation. We only record whether
// the workspace is already initialised; the offer toast fires async and
// the user can decline.
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (workspaceFolder) {
const initialized = isAxmeInitialized();
if (initialized) {
const counts = readCounts(workspaceFolder.uri.fsPath);
report.record("setup", true, `${counts.decisions} dec, ${counts.memories} mems`);
} else {
report.record("setup", true, "pending user action");
void offerSetupIfMissing(binary, "cursor");
}
} else {
report.record("setup", true, "no workspace open");
}

// ---- Step 7: status bar + commands -------------------------------------
statusBar = new AxmeStatusBar();
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (workspaceFolder) statusBar.attach(workspaceFolder.uri.fsPath);
context.subscriptions.push(statusBar);
context.subscriptions.push(
...registerCommands(context, binary, "cursor", statusBar),
);

// ---- Step 7b: reflect report state in status bar -----------------------
// The corner notification toast from report.present() auto-dismisses
// after 5s and is easy to miss. The status bar is always visible —
// setting it to "Setup required" (yellow) or "Activation failed" (red)
// when something needs attention is the user's continuous reminder.
if (report.hasFailure()) {
const labels = report.failedSteps().map((s) => s.kind).join(", ");
statusBar.setError(`${labels} failed`);
} else if (workspaceFolder && !isAxmeInitialized()) {
statusBar.setAttention("Setup required");
}

log(`Activation complete. ${context.subscriptions.length} disposables registered.`);

// ---- Step 8: present summary -------------------------------------------
await report.present();
}

export function deactivate(): void {
Expand Down
Loading
Loading