planmode 0.1.5 → 0.2.1

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.
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "planmode",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "The open source package manager for AI plans, rules, and prompts.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "planmode": "./dist/index.js"
7
+ "planmode": "./dist/index.js",
8
+ "planmode-mcp": "./dist/mcp.js"
8
9
  },
9
10
  "scripts": {
10
11
  "build": "tsup",
@@ -15,10 +16,12 @@
15
16
  "typecheck": "tsc --noEmit"
16
17
  },
17
18
  "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.26.0",
18
20
  "commander": "^13.1.0",
19
21
  "handlebars": "^4.7.8",
20
22
  "simple-git": "^3.27.0",
21
- "yaml": "^2.7.0"
23
+ "yaml": "^2.7.0",
24
+ "zod": "^4.3.6"
22
25
  },
23
26
  "devDependencies": {
24
27
  "@types/node": "^22.13.0",
@@ -0,0 +1,43 @@
1
+ import { Command } from "commander";
2
+ import { runDoctor } from "../lib/doctor.js";
3
+ import { logger } from "../lib/logger.js";
4
+
5
+ export const doctorCommand = new Command("doctor")
6
+ .description("Check project health: verify installed packages, imports, and file integrity")
7
+ .action(() => {
8
+ const result = runDoctor();
9
+
10
+ logger.blank();
11
+ logger.bold(`Checked ${result.packagesChecked} package(s)`);
12
+ logger.blank();
13
+
14
+ if (result.issues.length === 0) {
15
+ logger.success("Everything looks good. No issues found.");
16
+ logger.blank();
17
+ return;
18
+ }
19
+
20
+ const errors = result.issues.filter((i) => i.severity === "error");
21
+ const warnings = result.issues.filter((i) => i.severity === "warning");
22
+
23
+ for (const issue of errors) {
24
+ logger.error(issue.message);
25
+ if (issue.fix) logger.dim(` Fix: ${issue.fix}`);
26
+ }
27
+ for (const issue of warnings) {
28
+ logger.warn(issue.message);
29
+ if (issue.fix) logger.dim(` Fix: ${issue.fix}`);
30
+ }
31
+
32
+ logger.blank();
33
+ if (errors.length > 0) {
34
+ logger.error(`${errors.length} error(s), ${warnings.length} warning(s)`);
35
+ } else {
36
+ logger.warn(`${warnings.length} warning(s)`);
37
+ }
38
+ logger.blank();
39
+
40
+ if (errors.length > 0) {
41
+ process.exit(1);
42
+ }
43
+ });
@@ -1,8 +1,6 @@
1
1
  import { Command } from "commander";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { stringify } from "yaml";
5
2
  import { logger } from "../lib/logger.js";
3
+ import { createPackage } from "../lib/init.js";
6
4
  import type { PackageType, Category } from "../types/index.js";
7
5
 
8
6
  async function prompt(question: string): Promise<string> {
@@ -35,48 +33,31 @@ export const initCommand = new Command("init")
35
33
 
36
34
  const description = await prompt("Description: ");
37
35
  const author = await prompt("Author (GitHub username): ");
38
- const license = await prompt("License [MIT]: ") || "MIT";
36
+ const license = (await prompt("License [MIT]: ")) || "MIT";
39
37
  const tagsInput = await prompt("Tags (comma-separated): ");
40
- const tags = tagsInput ? tagsInput.split(",").map((t) => t.trim().toLowerCase()) : [];
41
- const category = (await prompt("Category (frontend/backend/devops/database/testing/mobile/ai-ml/security/other) [other]: ") || "other") as Category;
38
+ const tags = tagsInput
39
+ ? tagsInput.split(",").map((t) => t.trim().toLowerCase())
40
+ : [];
41
+ const category =
42
+ ((await prompt(
43
+ "Category (frontend/backend/devops/database/testing/mobile/ai-ml/security/other) [other]: ",
44
+ )) || "other") as Category;
42
45
 
43
- // Build manifest
44
- const manifest: Record<string, unknown> = {
46
+ const result = createPackage({
45
47
  name,
46
- version: "1.0.0",
47
48
  type,
48
49
  description,
49
50
  author,
50
51
  license,
51
- };
52
-
53
- if (tags.length > 0) manifest["tags"] = tags;
54
- manifest["category"] = category;
55
-
56
- const contentFile = `${type}.md`;
57
- manifest["content_file"] = contentFile;
58
-
59
- // Write planmode.yaml
60
- const yamlContent = stringify(manifest);
61
- fs.writeFileSync(path.join(process.cwd(), "planmode.yaml"), yamlContent, "utf-8");
62
- logger.success("Created planmode.yaml");
63
-
64
- // Write stub content file
65
- const stubs: Record<string, string> = {
66
- plan: `# ${name}\n\n1. First step\n2. Second step\n3. Third step\n`,
67
- rule: `- Rule one\n- Rule two\n- Rule three\n`,
68
- prompt: `Write your prompt here.\n\nUse {{variable_name}} for template variables.\n`,
69
- };
70
-
71
- fs.writeFileSync(
72
- path.join(process.cwd(), contentFile),
73
- stubs[type] ?? stubs["plan"]!,
74
- "utf-8",
75
- );
76
- logger.success(`Created ${contentFile}`);
52
+ tags,
53
+ category,
54
+ });
77
55
 
56
+ logger.success(`Created ${result.files.join(", ")}`);
78
57
  logger.blank();
79
- logger.info(`Edit ${contentFile}, then run \`planmode publish\` when ready.`);
58
+ logger.info(
59
+ `Edit ${result.files[1]}, then run \`planmode publish\` when ready.`,
60
+ );
80
61
  logger.blank();
81
62
  } catch (err) {
82
63
  logger.error((err as Error).message);
@@ -0,0 +1,39 @@
1
+ import { Command } from "commander";
2
+ import { execSync } from "node:child_process";
3
+ import { logger } from "../lib/logger.js";
4
+
5
+ export const mcpCommand = new Command("mcp")
6
+ .description("Manage MCP server registration with Claude Code");
7
+
8
+ mcpCommand
9
+ .command("setup")
10
+ .description("Register the planmode MCP server with Claude Code")
11
+ .action(() => {
12
+ try {
13
+ execSync("claude mcp add --transport stdio planmode -- planmode-mcp", {
14
+ stdio: "inherit",
15
+ });
16
+ logger.success("Planmode MCP server registered with Claude Code.");
17
+ logger.dim("Claude Code can now use planmode tools directly.");
18
+ } catch (err) {
19
+ logger.error(
20
+ "Failed to register MCP server. Make sure Claude Code CLI is installed.",
21
+ );
22
+ process.exit(1);
23
+ }
24
+ });
25
+
26
+ mcpCommand
27
+ .command("remove")
28
+ .description("Remove the planmode MCP server from Claude Code")
29
+ .action(() => {
30
+ try {
31
+ execSync("claude mcp remove planmode", { stdio: "inherit" });
32
+ logger.success("Planmode MCP server removed from Claude Code.");
33
+ } catch (err) {
34
+ logger.error(
35
+ "Failed to remove MCP server. Make sure Claude Code CLI is installed.",
36
+ );
37
+ process.exit(1);
38
+ }
39
+ });
@@ -1,201 +1,13 @@
1
1
  import { Command } from "commander";
2
- import { readManifest, validateManifest } from "../lib/manifest.js";
3
- import { getGitHubToken } from "../lib/config.js";
4
- import { getRemoteUrl, getHeadSha, createTag, pushTag } from "../lib/git.js";
2
+ import { publishPackage } from "../lib/publisher.js";
5
3
  import { logger } from "../lib/logger.js";
6
4
 
7
5
  export const publishCommand = new Command("publish")
8
6
  .description("Publish the current directory as a package to the registry")
9
7
  .action(async () => {
10
8
  try {
11
- const cwd = process.cwd();
12
-
13
- // Check auth
14
- const token = getGitHubToken();
15
- if (!token) {
16
- logger.error("Not authenticated. Run `planmode login` first.");
17
- process.exit(1);
18
- }
19
-
20
- // Read and validate manifest
21
- logger.info("Reading planmode.yaml...");
22
- const manifest = readManifest(cwd);
23
- const errors = validateManifest(manifest, true);
24
- if (errors.length > 0) {
25
- logger.error("Invalid manifest:");
26
- for (const err of errors) {
27
- console.log(` - ${err}`);
28
- }
29
- process.exit(1);
30
- }
31
-
32
- // Check git remote
33
- const remoteUrl = await getRemoteUrl(cwd);
34
- if (!remoteUrl) {
35
- logger.error("No git remote found. Push your code to GitHub first.");
36
- process.exit(1);
37
- }
38
-
39
- const sha = await getHeadSha(cwd);
40
- const tag = `v${manifest.version}`;
41
-
42
- // Create and push tag
43
- logger.info(`Creating tag ${tag}...`);
44
- try {
45
- await createTag(cwd, tag);
46
- } catch {
47
- logger.dim(`Tag ${tag} already exists, using existing`);
48
- }
49
-
50
- try {
51
- await pushTag(cwd, tag);
52
- logger.success(`Pushed tag ${tag}`);
53
- } catch {
54
- logger.dim(`Tag ${tag} already pushed`);
55
- }
56
-
57
- // Fork registry and create PR via GitHub API
58
- logger.info("Submitting to registry...");
59
-
60
- const headers = {
61
- Authorization: `Bearer ${token}`,
62
- Accept: "application/vnd.github.v3+json",
63
- "User-Agent": "planmode-cli",
64
- "Content-Type": "application/json",
65
- };
66
-
67
- // Fork the registry repo (idempotent)
68
- await fetch("https://api.github.com/repos/kaihannonen/planmode.org/forks", {
69
- method: "POST",
70
- headers,
71
- });
72
-
73
- // Get authenticated user
74
- const userRes = await fetch("https://api.github.com/user", { headers });
75
- const user = (await userRes.json()) as { login: string };
76
-
77
- // Create metadata files content
78
- const metadataContent = JSON.stringify(
79
- {
80
- name: manifest.name,
81
- description: manifest.description,
82
- author: manifest.author,
83
- license: manifest.license,
84
- repository: remoteUrl
85
- .replace(/^https?:\/\//, "")
86
- .replace(/\.git$/, ""),
87
- category: manifest.category ?? "other",
88
- tags: manifest.tags ?? [],
89
- type: manifest.type,
90
- models: manifest.models ?? [],
91
- latest_version: manifest.version,
92
- versions: [manifest.version],
93
- downloads: 0,
94
- created_at: new Date().toISOString(),
95
- updated_at: new Date().toISOString(),
96
- dependencies: manifest.dependencies,
97
- variables: manifest.variables,
98
- },
99
- null,
100
- 2,
101
- );
102
-
103
- const versionContent = JSON.stringify(
104
- {
105
- version: manifest.version,
106
- published_at: new Date().toISOString(),
107
- source: {
108
- repository: remoteUrl
109
- .replace(/^https?:\/\//, "")
110
- .replace(/\.git$/, ""),
111
- tag,
112
- sha,
113
- },
114
- files: ["planmode.yaml", manifest.content_file ?? "inline"],
115
- content_hash: `sha256:${sha.slice(0, 16)}`,
116
- },
117
- null,
118
- 2,
119
- );
120
-
121
- // Create branch on fork
122
- const branchName = `add-${manifest.name}-${manifest.version}`;
123
-
124
- // Get main branch ref
125
- const refRes = await fetch(
126
- `https://api.github.com/repos/${user.login}/planmode.org/git/ref/heads/main`,
127
- { headers },
128
- );
129
-
130
- if (!refRes.ok) {
131
- logger.error("Failed to access registry fork. Make sure the fork exists.");
132
- process.exit(1);
133
- }
134
-
135
- const refData = (await refRes.json()) as { object: { sha: string } };
136
- const baseSha = refData.object.sha;
137
-
138
- // Create branch
139
- await fetch(`https://api.github.com/repos/${user.login}/planmode.org/git/refs`, {
140
- method: "POST",
141
- headers,
142
- body: JSON.stringify({
143
- ref: `refs/heads/${branchName}`,
144
- sha: baseSha,
145
- }),
146
- });
147
-
148
- // Create metadata.json
149
- await fetch(
150
- `https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/metadata.json`,
151
- {
152
- method: "PUT",
153
- headers,
154
- body: JSON.stringify({
155
- message: `Add ${manifest.name}@${manifest.version}`,
156
- content: Buffer.from(metadataContent).toString("base64"),
157
- branch: branchName,
158
- }),
159
- },
160
- );
161
-
162
- // Create version file
163
- await fetch(
164
- `https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/versions/${manifest.version}.json`,
165
- {
166
- method: "PUT",
167
- headers,
168
- body: JSON.stringify({
169
- message: `Add ${manifest.name}@${manifest.version} version metadata`,
170
- content: Buffer.from(versionContent).toString("base64"),
171
- branch: branchName,
172
- }),
173
- },
174
- );
175
-
176
- // Create PR
177
- const prRes = await fetch("https://api.github.com/repos/kaihannonen/planmode.org/pulls", {
178
- method: "POST",
179
- headers,
180
- body: JSON.stringify({
181
- title: `Add ${manifest.name}@${manifest.version}`,
182
- head: `${user.login}:${branchName}`,
183
- base: "main",
184
- body: `## New package: ${manifest.name}\n\n- **Type:** ${manifest.type}\n- **Version:** ${manifest.version}\n- **Description:** ${manifest.description}\n- **Author:** ${manifest.author}\n\nSubmitted via \`planmode publish\`.`,
185
- }),
186
- });
187
-
188
- if (prRes.ok) {
189
- const pr = (await prRes.json()) as { html_url: string };
190
- logger.blank();
191
- logger.success(`Published ${manifest.name}@${manifest.version}`);
192
- logger.info(`PR: ${pr.html_url}`);
193
- } else {
194
- const err = await prRes.text();
195
- logger.error(`Failed to create PR: ${err}`);
196
- process.exit(1);
197
- }
198
-
9
+ logger.blank();
10
+ const result = await publishPackage();
199
11
  logger.blank();
200
12
  } catch (err) {
201
13
  logger.error((err as Error).message);
@@ -0,0 +1,76 @@
1
+ import { Command } from "commander";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { startRecordingAsync, stopRecording, isRecording } from "../lib/recorder.js";
5
+ import { logger } from "../lib/logger.js";
6
+
7
+ export const recordCommand = new Command("record")
8
+ .description("Record git activity and generate a plan from commits");
9
+
10
+ recordCommand
11
+ .command("start")
12
+ .description("Start recording — saves current HEAD as the starting point")
13
+ .action(async () => {
14
+ try {
15
+ logger.blank();
16
+ const sha = await startRecordingAsync();
17
+ logger.success(`Recording started at ${sha.slice(0, 7)}`);
18
+ logger.dim("Work normally. When done, run `planmode record stop` to generate a plan.");
19
+ logger.blank();
20
+ } catch (err) {
21
+ logger.error((err as Error).message);
22
+ process.exit(1);
23
+ }
24
+ });
25
+
26
+ recordCommand
27
+ .command("stop")
28
+ .description("Stop recording and generate a plan from commits since start")
29
+ .option("--name <name>", "Package name (auto-inferred if not provided)")
30
+ .option("--author <author>", "Author GitHub username")
31
+ .option("--dir <dir>", "Output directory for the generated package (default: current directory)")
32
+ .action(async (options: { name?: string; author?: string; dir?: string }) => {
33
+ try {
34
+ logger.blank();
35
+ logger.info("Analyzing commits...");
36
+
37
+ const result = await stopRecording(process.cwd(), {
38
+ name: options.name,
39
+ author: options.author,
40
+ });
41
+
42
+ // Write to output directory
43
+ const outDir = options.dir ?? process.cwd();
44
+ fs.mkdirSync(outDir, { recursive: true });
45
+
46
+ fs.writeFileSync(path.join(outDir, "planmode.yaml"), result.manifestContent, "utf-8");
47
+ fs.writeFileSync(path.join(outDir, "plan.md"), result.planContent, "utf-8");
48
+
49
+ logger.success(`Generated plan from ${result.totalCommits} commit(s) (${result.totalFilesChanged} files changed)`);
50
+ logger.blank();
51
+
52
+ for (let i = 0; i < result.steps.length; i++) {
53
+ const step = result.steps[i]!;
54
+ logger.dim(` ${i + 1}. ${step.title} (${step.filesChanged.length} files)`);
55
+ }
56
+
57
+ logger.blank();
58
+ logger.success("Created planmode.yaml and plan.md");
59
+ logger.dim("Edit the generated plan, then run `planmode test` to validate and `planmode publish` when ready.");
60
+ logger.blank();
61
+ } catch (err) {
62
+ logger.error((err as Error).message);
63
+ process.exit(1);
64
+ }
65
+ });
66
+
67
+ recordCommand
68
+ .command("status")
69
+ .description("Check if a recording is in progress")
70
+ .action(() => {
71
+ if (isRecording()) {
72
+ logger.info("Recording is in progress. Run `planmode record stop` to generate a plan.");
73
+ } else {
74
+ logger.info("No recording in progress.");
75
+ }
76
+ });
@@ -0,0 +1,46 @@
1
+ import { Command } from "commander";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { takeSnapshot } from "../lib/snapshot.js";
5
+ import { logger } from "../lib/logger.js";
6
+
7
+ export const snapshotCommand = new Command("snapshot")
8
+ .description("Analyze the current project and generate a plan that recreates this setup")
9
+ .option("--name <name>", "Package name (auto-inferred from project)")
10
+ .option("--author <author>", "Author GitHub username")
11
+ .option("--dir <dir>", "Output directory for the generated package (default: current directory)")
12
+ .action((options: { name?: string; author?: string; dir?: string }) => {
13
+ try {
14
+ logger.blank();
15
+ logger.info("Analyzing project...");
16
+
17
+ const result = takeSnapshot(process.cwd(), {
18
+ name: options.name,
19
+ author: options.author,
20
+ });
21
+
22
+ // Write to output directory
23
+ const outDir = options.dir ?? process.cwd();
24
+ fs.mkdirSync(outDir, { recursive: true });
25
+
26
+ fs.writeFileSync(path.join(outDir, "planmode.yaml"), result.manifestContent, "utf-8");
27
+ fs.writeFileSync(path.join(outDir, "plan.md"), result.planContent, "utf-8");
28
+
29
+ logger.blank();
30
+ logger.success(`Snapshot: ${result.data.name}`);
31
+ if (result.data.framework) {
32
+ logger.dim(` Framework: ${result.data.framework}`);
33
+ }
34
+ logger.dim(` Dependencies: ${Object.keys(result.data.dependencies).length}`);
35
+ logger.dim(` Dev dependencies: ${Object.keys(result.data.devDependencies).length}`);
36
+ logger.dim(` Tools detected: ${result.data.detectedTools.map((t) => t.name).join(", ") || "none"}`);
37
+
38
+ logger.blank();
39
+ logger.success("Created planmode.yaml and plan.md");
40
+ logger.dim("Edit the generated plan, then run `planmode test` to validate and `planmode publish` when ready.");
41
+ logger.blank();
42
+ } catch (err) {
43
+ logger.error((err as Error).message);
44
+ process.exit(1);
45
+ }
46
+ });
@@ -0,0 +1,45 @@
1
+ import { Command } from "commander";
2
+ import { testPackage } from "../lib/tester.js";
3
+ import { logger } from "../lib/logger.js";
4
+
5
+ export const testCommand = new Command("test")
6
+ .description("Test the current package before publishing: validate manifest, render templates, check dependencies")
7
+ .action(async () => {
8
+ try {
9
+ logger.blank();
10
+ logger.bold("Testing package...");
11
+ logger.blank();
12
+
13
+ const result = await testPackage();
14
+
15
+ for (const check of result.checks) {
16
+ if (check.passed) {
17
+ logger.success(check.name);
18
+ } else {
19
+ const issue = result.issues.find((i) => i.check === check.name);
20
+ if (issue?.severity === "error") {
21
+ logger.error(`${check.name}: ${issue.message}`);
22
+ } else if (issue) {
23
+ logger.warn(`${check.name}: ${issue.message}`);
24
+ }
25
+ }
26
+ }
27
+
28
+ logger.blank();
29
+ if (result.passed) {
30
+ logger.success(`All checks passed. Ready to publish.`);
31
+ } else {
32
+ const errors = result.issues.filter((i) => i.severity === "error");
33
+ const warnings = result.issues.filter((i) => i.severity === "warning");
34
+ logger.error(`${errors.length} error(s), ${warnings.length} warning(s). Fix errors before publishing.`);
35
+ }
36
+ logger.blank();
37
+
38
+ if (!result.passed) {
39
+ process.exit(1);
40
+ }
41
+ } catch (err) {
42
+ logger.error((err as Error).message);
43
+ process.exit(1);
44
+ }
45
+ });
package/src/index.ts CHANGED
@@ -9,13 +9,18 @@ import { listCommand } from "./commands/list.js";
9
9
  import { infoCommand } from "./commands/info.js";
10
10
  import { initCommand } from "./commands/init.js";
11
11
  import { loginCommand } from "./commands/login.js";
12
+ import { mcpCommand } from "./commands/mcp.js";
13
+ import { doctorCommand } from "./commands/doctor.js";
14
+ import { testCommand } from "./commands/test.js";
15
+ import { recordCommand } from "./commands/record.js";
16
+ import { snapshotCommand } from "./commands/snapshot.js";
12
17
 
13
18
  const program = new Command();
14
19
 
15
20
  program
16
21
  .name("planmode")
17
22
  .description("The open source package manager for AI plans, rules, and prompts.")
18
- .version("0.1.5");
23
+ .version("0.2.1");
19
24
 
20
25
  program.addCommand(installCommand);
21
26
  program.addCommand(uninstallCommand);
@@ -27,5 +32,10 @@ program.addCommand(listCommand);
27
32
  program.addCommand(infoCommand);
28
33
  program.addCommand(initCommand);
29
34
  program.addCommand(loginCommand);
35
+ program.addCommand(mcpCommand);
36
+ program.addCommand(doctorCommand);
37
+ program.addCommand(testCommand);
38
+ program.addCommand(recordCommand);
39
+ program.addCommand(snapshotCommand);
30
40
 
31
41
  program.parse();