Skip to content

Commit db687ab

Browse files
committed
feat(extension): v0.0.2 — activation summary, healthcheck webview, self-test, reset
Five quality-of-life features that close the silent-failure UX hole surfaced during v0.0.1 install (users had no clear signal when hooks or auth quietly failed during activation): 1. ActivationReport + per-step failure surfacing (extension) - extension/src/activation-report.ts: small class that tracks ok/fail per step (binary, mcp, hooks, auth, setup). At the end of activate(), shows a single info notification on full success ("AXME Code ready: ✓ MCP ✓ Hooks ✓ Auth (cursor_sdk) ✓ KB (33 dec, 6 mems)") OR a warning with [Show output] button when anything failed. - extension.ts: every step now wrapped in runStep() helper that records to the report AND surfaces a per-step warning toast on failure. Previously hooks/auth/setup errors landed only in the output channel with no user-visible signal. 2. AXME: Show Status → webview (extension) - extension/src/status-webview.ts: structured healthcheck panel with rows for extension version, binary, MCP server boot probe (spawns serve + initialize handshake), hooks file (parses ~/.cursor/hooks.json, counts axme entries per kind), auth mode, KB stats per workspace, last audit timestamp. Each row has a ✓/⚠/✗ status icon. Refresh button on the panel. - commands.ts: axme.showStatus now opens the webview instead of dumping to output channel. Old text-dump is preserved as axme.showStatusText command for power users. 3. axme-code self-test CLI subcommand (core) - src/self-test.ts: probes four runtime contracts — • atomicWrite to a temp .axme-code/ path • Cursor + Claude hook adapter parse (verifies Shell→Bash normalization) and deny emit shapes (Cursor: exit 2 + flat JSON; Claude: exit 0 + hookSpecificOutput) • MCP server boot via stdio initialize handshake (5s timeout) - cli.ts: 'self-test' case routes to runSelfTest(). Exits 0 on pass, 1 on any failure. Designed for CI scripts and for users debugging an install from terminal. - Smoke-tested: bundled .vsix binary passes 6/6 checks (storage write, parse Cursor, deny Cursor, parse Claude, deny Claude, MCP boot). 4. AXME: Reset command (extension) - extension/src/reset.ts: confirm-dialog command that clears ~/.cursor/hooks.json axme entries + ~/.config/axme-code/auth.yaml. Optional flag to also wipe ~/.config/axme-code/cursor.yaml (Cursor SDK key). Per-project .axme-code/ storage is NEVER touched — that's the user's curated knowledge. - commands.ts + package.json contribute axme.reset. 5. Misc package.json polish - extension version bump 0.0.1 → 0.0.2. - New commands surfaced in command palette: axme.showStatusText, axme.reauthAuditor, axme.reset. What this does NOT include (deferred to v0.0.3): - VS Code branch with cooperative axme_safety_check tool - @cursor/sdk bundled into per-platform .vsix - vscode-test framework + headless e2e - Welcome view / walkthrough Verified locally: bundled binary self-test 6/6 pass. tsc clean. .vsix builds at 519 KB. #!axme pr=none repo=AxmeAI/axme-code
1 parent 589b53d commit db687ab

8 files changed

Lines changed: 925 additions & 41 deletions

File tree

extension/package.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "axme-code",
33
"displayName": "AXME Code",
44
"description": "Persistent memory, decisions, and safety guardrails for Cursor, GitHub Copilot, Cline, Continue, Roo Code, Windsurf, and VS Code chat agents",
5-
"version": "0.0.1",
5+
"version": "0.0.2",
66
"publisher": "AxmeAI",
77
"repository": {
88
"type": "git",
@@ -80,6 +80,21 @@
8080
"command": "axme.showStatus",
8181
"title": "AXME: Show status",
8282
"category": "AXME"
83+
},
84+
{
85+
"command": "axme.showStatusText",
86+
"title": "AXME: Show status (text output)",
87+
"category": "AXME"
88+
},
89+
{
90+
"command": "axme.reauthAuditor",
91+
"title": "AXME: Reauth auditor (paste new API key)",
92+
"category": "AXME"
93+
},
94+
{
95+
"command": "axme.reset",
96+
"title": "AXME: Reset (clear hooks + auth on this machine)",
97+
"category": "AXME"
8398
}
8499
]
85100
},

extension/src/activation-report.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Activation status report — tracks success/failure of each step during
3+
* `activate()` so we can surface a clear summary at the end.
4+
*
5+
* Without this, a half-broken install (e.g. hooks failed to write, auditor
6+
* auth never persisted) leaves the user with no visible signal that
7+
* something is wrong — they only find out hours later when an action that
8+
* should have been blocked goes through, or when audit doesn't run.
9+
*
10+
* The report is rendered as:
11+
* - one-line summary toast when everything succeeded:
12+
* "AXME Code ready: ✓ MCP ✓ Hooks ✓ Auth (claude) ✓ KB (33 dec, 6 mems)"
13+
* - notification with [Show output] button when ANY step failed:
14+
* "AXME Code: partial install — Hooks ✗ (see output)"
15+
*
16+
* Each step records (kind, ok, detail). The summary is built when
17+
* activation completes.
18+
*/
19+
20+
import * as vscode from "vscode";
21+
import { show as showOutput } from "./log.js";
22+
23+
export type StepKind = "mcp" | "hooks" | "auth" | "setup" | "binary";
24+
25+
interface Step {
26+
kind: StepKind;
27+
ok: boolean;
28+
detail: string;
29+
}
30+
31+
const LABELS: Record<StepKind, string> = {
32+
binary: "Binary",
33+
mcp: "MCP",
34+
hooks: "Hooks",
35+
auth: "Auth",
36+
setup: "KB",
37+
};
38+
39+
export class ActivationReport {
40+
private readonly steps: Step[] = [];
41+
42+
record(kind: StepKind, ok: boolean, detail: string): void {
43+
this.steps.push({ kind, ok, detail });
44+
}
45+
46+
/** Render success/failure summary as a single line. */
47+
summary(): string {
48+
return this.steps
49+
.map((s) => `${s.ok ? "✓" : "✗"} ${LABELS[s.kind]}${s.detail ? ` (${s.detail})` : ""}`)
50+
.join(" ");
51+
}
52+
53+
hasFailure(): boolean {
54+
return this.steps.some((s) => !s.ok);
55+
}
56+
57+
failedSteps(): Step[] {
58+
return this.steps.filter((s) => !s.ok);
59+
}
60+
61+
/**
62+
* Show the result to the user. Non-modal in both branches. Success path
63+
* is dismissed automatically after VS Code's notification timeout; failure
64+
* path has a "Show output" button so the user can inspect the AXME Code
65+
* channel for the actual error.
66+
*/
67+
async present(): Promise<void> {
68+
const text = this.summary();
69+
if (!this.hasFailure()) {
70+
// Success — short info notification.
71+
void vscode.window.showInformationMessage(`AXME Code ready: ${text}`);
72+
return;
73+
}
74+
const failedNames = this.failedSteps().map((s) => LABELS[s.kind]).join(", ");
75+
const choice = await vscode.window.showWarningMessage(
76+
`AXME Code: partial install — ${failedNames} ${this.failedSteps().length === 1 ? "failed" : "failed"}. ${text}`,
77+
"Show output",
78+
"Dismiss",
79+
);
80+
if (choice === "Show output") showOutput();
81+
}
82+
}

extension/src/commands.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { IdeKind } from "./ide-detect.js";
1414
import { runSetup } from "./setup-controller.js";
1515
import { ensureAuditorAuth } from "./auditor-auth.js";
1616
import { AxmeStatusBar } from "./status-bar.js";
17+
import { openStatusWebview } from "./status-webview.js";
18+
import { runReset } from "./reset.js";
1719
import { log, logError, show as showOutput } from "./log.js";
1820

1921
function workspaceRoot(): string | undefined {
@@ -67,6 +69,14 @@ export function registerCommands(
6769
}),
6870

6971
vscode.commands.registerCommand("axme.showStatus", async () => {
72+
// v0.0.2: replace plain-text output dump with a full healthcheck
73+
// webview (status of binary, MCP, hooks, auth, KB per workspace).
74+
// The old "axme-code status" output dump is still accessible via the
75+
// axme.showStatusText fallback command for power users.
76+
await openStatusWebview(binary);
77+
}),
78+
79+
vscode.commands.registerCommand("axme.showStatusText", async () => {
7080
const root = workspaceRoot();
7181
if (!root) {
7282
void vscode.window.showWarningMessage("AXME Code: open a folder first.");
@@ -116,5 +126,9 @@ export function registerCommands(
116126
await vscode.window.showTextDocument(doc);
117127
}
118128
}),
129+
130+
vscode.commands.registerCommand("axme.reset", async () => {
131+
await runReset();
132+
}),
119133
];
120134
}

extension/src/extension.ts

Lines changed: 95 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* AXME Code — Cursor extension entry point (v0.0.1, Cursor-only).
2+
* AXME Code — Cursor extension entry point (v0.0.2, Cursor-only).
33
*
44
* Activation flow:
55
* 1. Detect Cursor (vs VS Code or other fork). Bail out with a friendly
@@ -11,12 +11,17 @@
1111
* 6. If the workspace is not initialised yet, offer to run `axme-code setup`.
1212
* 7. Attach the AXME status bar and register commands.
1313
*
14+
* Every step's outcome is recorded in an ActivationReport; at the end we
15+
* show a single summary toast with ✓/✗ per step so the user can see at a
16+
* glance that everything is wired up (or which step needs attention).
17+
* Without this, partial installs used to fail silently — the user only
18+
* noticed hours later when a safety hook didn't fire or an audit didn't
19+
* run.
20+
*
1421
* Deactivation disposes the MCP registration (Cursor unregisters the
1522
* server), the status bar, the FS watcher, and all commands. User-level
1623
* hooks at ~/.cursor/hooks.json are NOT removed on deactivate — the user
17-
* can remove them manually if they uninstall the extension. (VS Code's
18-
* deactivate fires on plain window-close too, so blanket-removing hooks
19-
* there would be wrong.)
24+
* can clear them via `AXME: Reset` command if uninstalling.
2025
*/
2126

2227
import * as vscode from "vscode";
@@ -25,26 +30,59 @@ import { findAxmeBinary } from "./binary-detect.js";
2530
import { registerMcpServer } from "./mcp-register.js";
2631
import { installUserHooks } from "./hooks-install.js";
2732
import { ensureAuditorAuth } from "./auditor-auth.js";
28-
import { offerSetupIfMissing } from "./setup-controller.js";
33+
import { offerSetupIfMissing, isAxmeInitialized } from "./setup-controller.js";
2934
import { AxmeStatusBar } from "./status-bar.js";
3035
import { registerCommands } from "./commands.js";
36+
import { readCounts } from "./kb-watcher.js";
37+
import { ActivationReport, StepKind } from "./activation-report.js";
3138
import { log, logError, show as showOutput, dispose as disposeLog } from "./log.js";
3239

3340
declare const __EXTENSION_VERSION__: string;
3441

3542
let statusBar: AxmeStatusBar | undefined;
3643

44+
/**
45+
* Run an activation step inside a try/catch. On failure: record the step
46+
* as failed in the report AND surface a non-modal warning notification
47+
* with a [Show output] button so the user is not left guessing. The
48+
* activation flow continues past the failure — partial functionality is
49+
* better than total failure (e.g. MCP registered but hooks failed → user
50+
* still gets axme tools, just no machine-wide safety blocks).
51+
*/
52+
async function runStep<T>(
53+
report: ActivationReport,
54+
kind: StepKind,
55+
successDetail: (result: T) => string,
56+
body: () => Promise<T>,
57+
): Promise<T | undefined> {
58+
try {
59+
const result = await body();
60+
report.record(kind, true, successDetail(result));
61+
return result;
62+
} catch (err) {
63+
logError(`Step ${kind}`, err);
64+
report.record(kind, false, (err as Error).message.slice(0, 80));
65+
void vscode.window
66+
.showWarningMessage(
67+
`AXME Code: ${kind} step failed — ${(err as Error).message}`,
68+
"Show output",
69+
)
70+
.then((c) => { if (c === "Show output") showOutput(); });
71+
return undefined;
72+
}
73+
}
74+
3775
export async function activate(context: vscode.ExtensionContext): Promise<void> {
3876
log(`AXME Code v${__EXTENSION_VERSION__} activating…`);
3977

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

58-
// ---- Step 2: binary -----------------------------------------------------
59-
const binary = await findAxmeBinary(context);
96+
const report = new ActivationReport();
97+
98+
// ---- Step 2: binary detection ------------------------------------------
99+
const binary = await runStep(report, "binary", (b) => b.split("/").pop() ?? "ok", async () => {
100+
const path = await findAxmeBinary(context);
101+
if (!path) {
102+
throw new Error(
103+
"bundled axme-code binary not found inside this .vsix. " +
104+
"Reinstall the extension; if the problem persists, file an issue.",
105+
);
106+
}
107+
log(` Binary: ${path}`);
108+
return path;
109+
});
60110
if (!binary) {
61-
log(" axme-code binary not found.");
62-
void vscode.window.showErrorMessage(
63-
"AXME Code: bundled axme-code binary not found inside this .vsix. " +
64-
"Please file an issue at github.com/AxmeAI/axme-code/issues. As a " +
65-
"workaround, install axme-code separately and set `axme.binaryPath`.",
66-
);
111+
// Binary missing is fatal — every other step needs it. Show summary
112+
// anyway so user sees the failure in toast form.
113+
await report.present();
67114
return;
68115
}
69-
log(` Binary: ${binary}`);
70116

71117
// ---- Step 3: MCP registration ------------------------------------------
72-
try {
73-
const mcpDisposable = await registerMcpServer(binary);
74-
context.subscriptions.push(mcpDisposable);
75-
} catch (err) {
76-
logError("MCP register", err);
77-
void vscode.window.showErrorMessage(
78-
`AXME Code: MCP registration failed — ${(err as Error).message}. ` +
79-
"See AXME Code output channel.",
80-
);
81-
// Continue activation so user can still see output + try Reauth / Setup.
82-
}
118+
await runStep(report, "mcp", () => "registered", async () => {
119+
const disposable = await registerMcpServer(binary);
120+
context.subscriptions.push(disposable);
121+
});
83122

84123
// ---- Step 4: hooks ------------------------------------------------------
85124
const enableHooks = vscode.workspace
86125
.getConfiguration("axme")
87126
.get<boolean>("enableHooks", true);
88127
if (enableHooks) {
89-
try {
90-
installUserHooks("cursor", binary);
91-
} catch (err) {
92-
logError("Hooks install", err);
93-
}
128+
await runStep(report, "hooks", () => "user-level", async () => {
129+
const ok = installUserHooks("cursor", binary);
130+
if (!ok) throw new Error("hooks install returned false");
131+
});
94132
} else {
95133
log("Hooks: disabled by axme.enableHooks setting");
134+
report.record("hooks", true, "disabled by setting");
96135
}
97136

98137
// ---- Step 5: auditor auth ----------------------------------------------
99-
try {
100-
await ensureAuditorAuth(binary);
101-
} catch (err) {
102-
logError("Auditor auth", err);
103-
}
138+
await runStep(report, "auth", (mode) => mode ?? "?", async () => {
139+
const mode = await ensureAuditorAuth(binary);
140+
return mode;
141+
});
104142

105-
// ---- Step 6: setup offer -----------------------------------------------
106-
void offerSetupIfMissing(binary, "cursor");
143+
// ---- Step 6: setup offer (non-blocking, fire-and-forget) ---------------
144+
// Setup is the user's job, not part of activation. We only record whether
145+
// the workspace is already initialised; the offer toast fires async and
146+
// the user can decline.
147+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
148+
if (workspaceFolder) {
149+
const initialized = isAxmeInitialized();
150+
if (initialized) {
151+
const counts = readCounts(workspaceFolder.uri.fsPath);
152+
report.record("setup", true, `${counts.decisions} dec, ${counts.memories} mems`);
153+
} else {
154+
report.record("setup", true, "pending user action");
155+
void offerSetupIfMissing(binary, "cursor");
156+
}
157+
} else {
158+
report.record("setup", true, "no workspace open");
159+
}
107160

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

117169
log(`Activation complete. ${context.subscriptions.length} disposables registered.`);
170+
171+
// ---- Step 8: present summary -------------------------------------------
172+
await report.present();
118173
}
119174

120175
export function deactivate(): void {

0 commit comments

Comments
 (0)