Skip to content

Commit 87b1c63

Browse files
authored
Merge pull request #82 from AxmeAI/feat/plugin-bundle-20260408
feat: self-contained plugin bundle for Claude Code marketplace
2 parents 4c999a0 + f237169 commit 87b1c63

12 files changed

Lines changed: 214 additions & 37 deletions

File tree

build.mjs

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,116 @@ await build({
3232
});
3333

3434
// Create bin wrapper
35-
import { writeFileSync, chmodSync } from "fs";
35+
import { writeFileSync, chmodSync, mkdirSync } from "fs";
3636
writeFileSync("dist/axme-code.js", '#!/usr/bin/env node\nimport("./cli.mjs");\n');
3737
chmodSync("dist/axme-code.js", 0o755);
3838

39+
// --- Plugin bundled builds (self-contained, zero external deps) ---
40+
41+
await build({
42+
entryPoints: ["src/server.ts"],
43+
bundle: true,
44+
platform: "node",
45+
target: "node20",
46+
format: "esm",
47+
packages: "bundle",
48+
outfile: "dist/plugin/server.mjs",
49+
sourcemap: true,
50+
define,
51+
});
52+
53+
await build({
54+
entryPoints: ["src/cli.ts"],
55+
bundle: true,
56+
platform: "node",
57+
target: "node20",
58+
format: "esm",
59+
packages: "bundle",
60+
external: ["@anthropic-ai/claude-agent-sdk"],
61+
outfile: "dist/plugin/cli.mjs",
62+
sourcemap: true,
63+
banner: { js: "" },
64+
define,
65+
});
66+
67+
// Plugin bin wrapper — sets NODE_PATH so SDK can be found from CLAUDE_PLUGIN_DATA
68+
mkdirSync("dist/plugin/bin", { recursive: true });
69+
writeFileSync("dist/plugin/bin/axme-code", `#!/bin/bash
70+
PLUGIN_DIR="\$(cd "\$(dirname "\$0")/.." && pwd)"
71+
DATA_DIR="\${CLAUDE_PLUGIN_DATA:-\$HOME/.claude/plugins/data/axme-code}"
72+
export NODE_PATH="\$DATA_DIR/node_modules:\$NODE_PATH"
73+
exec node "\$PLUGIN_DIR/cli.mjs" "\$@"
74+
`);
75+
chmodSync("dist/plugin/bin/axme-code", 0o755);
76+
77+
// Plugin package.json — only SDK for npm install in CLAUDE_PLUGIN_DATA
78+
writeFileSync("dist/plugin/package.json", JSON.stringify({
79+
name: "@axme/code-plugin",
80+
private: true,
81+
dependencies: {
82+
"@anthropic-ai/claude-agent-sdk": pkg.dependencies["@anthropic-ai/claude-agent-sdk"],
83+
},
84+
}, null, 2) + "\n");
85+
86+
// --- Assemble plugin directory ---
87+
import { cpSync, existsSync } from "fs";
88+
89+
mkdirSync("dist/plugin/.claude-plugin", { recursive: true });
90+
mkdirSync("dist/plugin/hooks", { recursive: true });
91+
92+
// Plugin manifest
93+
cpSync(".claude-plugin/plugin.json", "dist/plugin/.claude-plugin/plugin.json");
94+
95+
// Plugin .mcp.json — uses ${CLAUDE_PLUGIN_ROOT} for self-contained execution
96+
writeFileSync("dist/plugin/.mcp.json", JSON.stringify({
97+
mcpServers: {
98+
axme: {
99+
command: "node",
100+
args: ["${CLAUDE_PLUGIN_ROOT}/server.mjs"],
101+
env: {
102+
NODE_PATH: "${CLAUDE_PLUGIN_DATA}/node_modules",
103+
},
104+
},
105+
},
106+
}, null, 2) + "\n");
107+
108+
// Plugin hooks — safety enforcement via bundled CLI
109+
writeFileSync("dist/plugin/hooks/hooks.json", JSON.stringify({
110+
description: "AXME Code safety enforcement and session tracking",
111+
hooks: {
112+
SessionStart: [{
113+
hooks: [{
114+
type: "command",
115+
command: "diff -q ${CLAUDE_PLUGIN_ROOT}/package.json ${CLAUDE_PLUGIN_DATA}/package.json >/dev/null 2>&1 || (mkdir -p ${CLAUDE_PLUGIN_DATA} && cp ${CLAUDE_PLUGIN_ROOT}/package.json ${CLAUDE_PLUGIN_DATA}/ && cd ${CLAUDE_PLUGIN_DATA} && npm install --omit=dev --ignore-scripts 2>/dev/null) ; NODE_PATH=${CLAUDE_PLUGIN_DATA}/node_modules node ${CLAUDE_PLUGIN_ROOT}/cli.mjs check-init",
116+
timeout: 30,
117+
}],
118+
}],
119+
PreToolUse: [{
120+
hooks: [{
121+
type: "command",
122+
command: "node ${CLAUDE_PLUGIN_ROOT}/cli.mjs hook pre-tool-use",
123+
timeout: 5,
124+
}],
125+
}],
126+
PostToolUse: [{
127+
matcher: "Edit|Write|NotebookEdit",
128+
hooks: [{
129+
type: "command",
130+
command: "node ${CLAUDE_PLUGIN_ROOT}/cli.mjs hook post-tool-use",
131+
timeout: 10,
132+
}],
133+
}],
134+
SessionEnd: [{
135+
hooks: [{
136+
type: "command",
137+
command: "node ${CLAUDE_PLUGIN_ROOT}/cli.mjs hook session-end",
138+
timeout: 120,
139+
}],
140+
}],
141+
},
142+
}, null, 2) + "\n");
143+
144+
// Copy LICENSE and README
145+
if (existsSync("LICENSE")) cpSync("LICENSE", "dist/plugin/LICENSE");
146+
39147
console.log("Build complete.");

src/agents/scanners/decision.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ Read the project's documentation and code to find decisions that were made about
4848
- Check subdirectories for additional CLAUDE.md files
4949
- Claude auto-memory (accumulated operational knowledge):
5050
- Compute encoded path: replace non-alphanumeric chars in absolute project path with "-"
51-
- Read ~/.claude/projects/<encoded-path>/memory/MEMORY.md
52-
- Read ALL .md files in ~/.claude/projects/<encoded-path>/memory/
51+
- First check if directory exists: ls ~/.claude/projects/<encoded-path>/memory/ — skip if not found
52+
- If exists: read MEMORY.md and ALL .md files in that directory
5353
- These contain decisions made during real work - extract them
5454
- Architecture Decision Records (docs/adr/, docs/decisions/, docs/architecture/decisions/, adr/)
5555
- Architecture docs (docs/, ARCHITECTURE.md, DESIGN.md)

src/agents/scanners/deploy.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ Read these files if they exist:
3434
8. package.json (scripts section), pyproject.toml - build/test/deploy commands
3535
9. **Pre-deploy checklist files** - look for files with CHECKLIST, PRE_PROD, pre-deploy in name
3636
10. **CLAUDE.md** - read for deploy rules, staging/prod procedures, deploy prohibitions
37-
11. **Claude auto-memory** - check ~/.claude/projects/<encoded-path>/memory/ for deploy-related feedback
38-
(encoded-path = absolute project path with non-alphanumeric chars replaced by "-")
37+
11. **Claude auto-memory** - compute encoded-path (replace non-alphanumeric chars in absolute project path with "-"), check if ~/.claude/projects/<encoded-path>/memory/ exists (ls first), if yes read .md files for deploy-related feedback
3938
4039
## What to extract
4140

src/agents/scanners/oracle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ Thoroughly scan the project using the tools available to you. Read files, explor
4343
- Check subdirectories for additional CLAUDE.md files
4444
4. **Claude auto-memory (accumulated project knowledge):**
4545
- Compute the encoded project path: replace every non-alphanumeric char in the absolute project path with "-"
46-
- Check ~/.claude/projects/<encoded-path>/memory/MEMORY.md
47-
- Read ALL .md files in ~/.claude/projects/<encoded-path>/memory/
46+
- First check if the directory exists: ls ~/.claude/projects/<encoded-path>/memory/ — if it doesn't exist, skip this step entirely
47+
- If it exists: read MEMORY.md and ALL .md files in that directory
4848
- These contain hard-won operational lessons - treat as HIGH PRIORITY
4949
5. **Config files:** tsconfig.json, .eslintrc*, eslint.config.*, .prettierrc*, .editorconfig, Makefile, Taskfile.yml, Justfile
5050
6. **Source directory structure** (list all significant directories and their contents)

src/agents/scanners/safety.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ Read these files if they exist:
3636
- Also check .claude/CLAUDE.md, .claude/rules/*.md, .claudecode/rules.md
3737
- If CLAUDE.md references other files with rules - follow and read them
3838
11. **AGENTS.md** - may contain safety constraints
39-
12. **Claude auto-memory** - check ~/.claude/projects/<encoded-path>/memory/ for safety-related feedback
40-
(encoded-path = absolute project path with non-alphanumeric chars replaced by "-")
39+
12. **Claude auto-memory** - compute encoded-path (replace non-alphanumeric chars in absolute project path with "-"), check if ~/.claude/projects/<encoded-path>/memory/ exists (ls first), if yes read .md files for safety-related feedback
4140
4241
## What to extract
4342

src/cli.ts

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,8 @@ async function main() {
264264
switch (command) {
265265
case "setup": {
266266
const forceSetup = args.includes("--force");
267-
const setupArgs = args.filter(a => a !== "--force");
267+
const pluginMode = args.includes("--plugin") || !!process.env.CLAUDE_PLUGIN_ROOT;
268+
const setupArgs = args.filter(a => a !== "--force" && a !== "--plugin");
268269
const projectPath = resolve(setupArgs[1] || ".");
269270
const hasGitDir = existsSync(join(projectPath, ".git"));
270271
const ws = detectWorkspace(projectPath);
@@ -315,31 +316,43 @@ async function main() {
315316
}
316317
}
317318

318-
// Create or update .mcp.json (workspace root + each child repo)
319-
const mcpEntry = { command: "axme-code", args: ["serve"] };
320-
const mcpPaths = [projectPath];
321-
if (isWorkspace) {
322-
for (const p of ws.projects) {
323-
mcpPaths.push(join(projectPath, p.path));
319+
// Detect plugin context — skip .mcp.json and hooks if running from plugin
320+
// (plugin provides its own .mcp.json and hooks/hooks.json)
321+
const isPlugin = pluginMode;
322+
323+
if (!isPlugin) {
324+
// Create or update .mcp.json (workspace root + each child repo)
325+
const mcpEntry = { command: "axme-code", args: ["serve"] };
326+
const mcpPaths = [projectPath];
327+
if (isWorkspace) {
328+
for (const p of ws.projects) {
329+
mcpPaths.push(join(projectPath, p.path));
330+
}
324331
}
325-
}
326-
for (const dir of mcpPaths) {
327-
const mcpPath = join(dir, ".mcp.json");
328-
let mcpConfig: Record<string, any> = {};
329-
if (existsSync(mcpPath)) {
330-
try { mcpConfig = JSON.parse(readFileSync(mcpPath, "utf-8")); } catch { mcpConfig = {}; }
332+
for (const dir of mcpPaths) {
333+
const mcpPath = join(dir, ".mcp.json");
334+
let mcpConfig: Record<string, any> = {};
335+
if (existsSync(mcpPath)) {
336+
try { mcpConfig = JSON.parse(readFileSync(mcpPath, "utf-8")); } catch { mcpConfig = {}; }
337+
}
338+
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
339+
mcpConfig.mcpServers.axme = mcpEntry;
340+
writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
331341
}
332-
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
333-
mcpConfig.mcpServers.axme = mcpEntry;
334-
writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
342+
console.log(` .mcp.json: updated (${mcpPaths.length} locations)`);
343+
} else {
344+
console.log(` .mcp.json: skipped (plugin provides MCP server)`);
335345
}
336-
console.log(` .mcp.json: updated (${mcpPaths.length} locations)`);
337346

338347
// Generate CLAUDE.md
339348
generateClaudeMd(projectPath, isWorkspace);
340349

341-
// Configure Claude Code hooks in .claude/settings.json
342-
configureHooks(projectPath);
350+
if (!isPlugin) {
351+
// Configure Claude Code hooks in .claude/settings.json
352+
configureHooks(projectPath);
353+
} else {
354+
console.log(` Hooks: skipped (plugin provides hooks)`);
355+
}
343356

344357
// Add .axme-code/ to .gitignore
345358
const gitignorePath = join(projectPath, ".gitignore");
@@ -394,6 +407,36 @@ async function main() {
394407
break;
395408
}
396409

410+
case "check-init": {
411+
// Plugin SessionStart hook — ensures CLAUDE.md exists and outputs instruction
412+
const checkPath = resolve(args[1] || ".");
413+
const claudeMdPath = join(checkPath, "CLAUDE.md");
414+
const axmeSection = `## AXME Code
415+
416+
### Session Start (MANDATORY)
417+
Call axme_context at the start of every session.
418+
If it returns "not initialized", run \`axme-code setup --plugin\` via Bash tool immediately.
419+
Do NOT skip — without context you will miss critical project rules.
420+
`;
421+
// Ensure CLAUDE.md has AXME section
422+
if (existsSync(claudeMdPath)) {
423+
const content = readFileSync(claudeMdPath, "utf-8");
424+
if (!content.includes("## AXME Code")) {
425+
writeFileSync(claudeMdPath, content.trimEnd() + "\n\n" + axmeSection, "utf-8");
426+
}
427+
} else {
428+
writeFileSync(claudeMdPath, axmeSection, "utf-8");
429+
}
430+
431+
const { configExists } = await import("./storage/config.js");
432+
if (configExists(checkPath)) {
433+
console.log(`[AXME Code] Knowledge base ready. Call axme_context now.`);
434+
} else {
435+
console.log(`[AXME Code] Project not initialized. Run: axme-code setup --plugin`);
436+
}
437+
break;
438+
}
439+
397440
case "audit-session": {
398441
// Standalone entry point for the detached audit worker. Takes the
399442
// workspace path and an AXME session id, runs runSessionCleanup on

src/hooks/post-tool-use.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ function handlePostToolUse(workspacePath: string, event: HookInput): void {
5454
* @param workspacePath - from --workspace CLI flag
5555
*/
5656
export async function runPostToolUseHook(workspacePath?: string): Promise<void> {
57-
if (!workspacePath) return; // No workspace = nothing to do
57+
if (!workspacePath) workspacePath = process.cwd();
58+
if (!workspacePath) return;
5859

5960
// Skip entirely when we are running inside a subclaude audit worker
6061
// (see session-auditor env: { ...process.env, AXME_SKIP_HOOKS: "1" }).

src/hooks/pre-tool-use.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ function handlePreToolUse(sessionOrigin: string, event: HookInput): void {
202202
* @param workspacePath - from --workspace CLI flag
203203
*/
204204
export async function runPreToolUseHook(workspacePath?: string): Promise<void> {
205+
if (!workspacePath) workspacePath = process.cwd();
205206
if (!workspacePath) return;
206207

207208
// Subclaude audit workers run inside session-auditor with

src/hooks/session-end.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ function handleSessionEnd(workspacePath: string, input: SessionEndInput): void {
6767
* @param workspacePath - from --workspace CLI flag
6868
*/
6969
export async function runSessionEndHook(workspacePath?: string): Promise<void> {
70+
if (!workspacePath) workspacePath = process.cwd();
7071
if (!workspacePath) return;
7172

7273
// Skip entirely when running inside a subclaude audit worker (see

src/server.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -738,18 +738,18 @@ server.tool(
738738
})).optional().describe("Safety rules to add/remove"),
739739
// --- Handoff ---
740740
stopped_at: z.string().describe("What the session stopped at (single line)"),
741-
summary: z.string().describe("2-5 bullet points of what was accomplished"),
742-
in_progress: z.string().describe("Current state: branches, PRs, uncommitted work"),
741+
summary: z.string().describe("2-5 bullet points of what was accomplished. Use real newlines, NOT literal backslash-n. Each bullet on its own line starting with '- '."),
742+
in_progress: z.string().describe("Current state: branches, PRs, uncommitted work. Use real newlines, NOT literal backslash-n."),
743743
prs: z.array(z.object({
744744
url: z.string(),
745745
title: z.string(),
746746
status: z.string(),
747747
})).optional().describe("PRs created/merged in this session"),
748748
test_results: z.string().optional().describe("Test run summary"),
749749
blockers: z.string().optional().describe("Blockers for next session"),
750-
next_steps: z.string().describe("Concrete next steps for next session"),
750+
next_steps: z.string().describe("Concrete next steps for next session. Use real newlines, NOT literal backslash-n."),
751751
dirty_branches: z.string().optional().describe("Branch names with state"),
752-
worklog_entry: z.string().describe("Narrative session summary (5-15 lines markdown)"),
752+
worklog_entry: z.string().describe("Narrative session summary (5-15 lines markdown). Use real newlines, NOT literal backslash-n."),
753753
startup_text: z.string().describe("Ready-to-paste startup text for the next session"),
754754
},
755755
async (args) => {
@@ -879,6 +879,7 @@ server.tool(
879879
const session = loadSession(targetPath, sid);
880880
if (session) {
881881
session.agentClosed = true;
882+
session.closedAt = new Date().toISOString();
882883
writeSession(targetPath, session);
883884
}
884885

0 commit comments

Comments
 (0)