pmflow-cli 0.1.0__py3-none-any.whl

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.
pmflow_cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """pmflow-cli: Terminal client for pmflow."""
pmflow_cli/client.py ADDED
@@ -0,0 +1,21 @@
1
+ import httpx
2
+
3
+ from pmflow_shared.constants import API_V1_PREFIX
4
+
5
+ from pmflow_cli.config import CliConfig
6
+
7
+
8
+ class PmflowClient:
9
+ def __init__(self, config: CliConfig | None = None):
10
+ self.config = config or CliConfig.load()
11
+ self.http = httpx.AsyncClient(
12
+ base_url=f"{self.config.server_url}{API_V1_PREFIX}",
13
+ headers={"Authorization": f"Bearer {self.config.api_token}"},
14
+ timeout=30.0,
15
+ )
16
+
17
+ async def __aenter__(self):
18
+ return self
19
+
20
+ async def __aexit__(self, *args):
21
+ await self.http.aclose()
File without changes
@@ -0,0 +1,16 @@
1
+ import typer
2
+
3
+ app = typer.Typer(name="ai", help="AI-assisted features")
4
+
5
+
6
+ @app.command()
7
+ def chat(seq_num: int):
8
+ """Start interactive AI conversation for an issue."""
9
+ raise NotImplementedError
10
+
11
+
12
+
13
+ @app.command()
14
+ def conversations(seq_num: int):
15
+ """List AI conversations for an issue."""
16
+ raise NotImplementedError
@@ -0,0 +1,9 @@
1
+ import typer
2
+
3
+ app = typer.Typer(name="auth", help="Authentication commands")
4
+
5
+
6
+ @app.command()
7
+ def token():
8
+ """Manage API tokens."""
9
+ raise NotImplementedError
@@ -0,0 +1,271 @@
1
+ import asyncio
2
+ import json
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import typer
9
+
10
+ from pmflow_cli.client import PmflowClient
11
+ from pmflow_cli.config import CliConfig
12
+ from pmflow_cli.output import console, print_table
13
+
14
+ FEISHU_DOC_BASE_URL = "https://bytedance.larkoffice.com/docx"
15
+
16
+ app = typer.Typer(name="ctx", help="Context management")
17
+
18
+
19
+ def _get_workspace(workspace: str | None) -> str:
20
+ """Get workspace slug from option or config."""
21
+ if workspace:
22
+ return workspace
23
+ config = CliConfig.load()
24
+ if not config.workspace:
25
+ console.print("[red]Error:[/red] No workspace specified. Use --workspace or set default with `pmflow ws switch`.")
26
+ raise typer.Exit(1)
27
+ return config.workspace
28
+
29
+
30
+ async def _list_issue_contexts(slug: str, seq_num: int) -> list[dict]:
31
+ async with PmflowClient() as client:
32
+ resp = await client.http.get(f"/workspaces/{slug}/issues/{seq_num}/contexts")
33
+ resp.raise_for_status()
34
+ return resp.json()
35
+
36
+
37
+ async def _list_space_contexts(slug: str) -> list[dict]:
38
+ async with PmflowClient() as client:
39
+ resp = await client.http.get(f"/workspaces/{slug}/contexts")
40
+ resp.raise_for_status()
41
+ return resp.json()
42
+
43
+
44
+ def _create_feishu_doc(title: str) -> tuple[str, str]:
45
+ """Create a new feishu document via lark-cli. Returns (doc_token, doc_url)."""
46
+ result = subprocess.run(
47
+ ["lark-cli", "docs", "+create", "--title", title, "--markdown", ""],
48
+ capture_output=True,
49
+ text=True,
50
+ )
51
+ if result.returncode != 0:
52
+ raise RuntimeError(result.stderr.strip() or f"lark-cli exited with code {result.returncode}")
53
+ data = json.loads(result.stdout.strip())
54
+ inner = data.get("data", data)
55
+ doc_token = inner.get("doc_id") or inner.get("document_id") or inner.get("doc_token")
56
+ doc_url = inner.get("doc_url") or f"{FEISHU_DOC_BASE_URL}/{doc_token}"
57
+ return doc_token, doc_url
58
+
59
+
60
+ def _update_feishu_doc(doc_token: str, content: str) -> None:
61
+ """Update feishu document content via lark-cli."""
62
+ result = subprocess.run(
63
+ ["lark-cli", "docs", "+update", "--doc", doc_token, "--markdown", content, "--mode", "overwrite"],
64
+ capture_output=True,
65
+ text=True,
66
+ )
67
+ if result.returncode != 0:
68
+ raise RuntimeError(result.stderr.strip() or f"lark-cli exited with code {result.returncode}")
69
+
70
+
71
+ def _fetch_feishu_doc(doc_token: str) -> str:
72
+ """Fetch feishu document content as markdown via lark-cli directly."""
73
+ result = subprocess.run(
74
+ ["lark-cli", "docs", "+fetch", "--doc", doc_token],
75
+ capture_output=True,
76
+ text=True,
77
+ )
78
+ if result.returncode != 0:
79
+ raise RuntimeError(result.stderr.strip() or f"lark-cli exited with code {result.returncode}")
80
+ raw = result.stdout.strip()
81
+ try:
82
+ data = json.loads(raw)
83
+ return data.get("data", {}).get("markdown", "") or ""
84
+ except json.JSONDecodeError:
85
+ return raw
86
+
87
+
88
+ def _print_contexts(contexts: list[dict], title: str, format: str):
89
+ if format == "json":
90
+ console.print_json(json.dumps(contexts))
91
+ return
92
+ if not contexts:
93
+ console.print("No contexts found.")
94
+ return
95
+ print_table(
96
+ columns=["Type", "Title", "Doc Token", "URL"],
97
+ rows=[
98
+ [
99
+ c.get("type", ""),
100
+ c.get("title", ""),
101
+ c.get("feishu_doc_token", "") or "",
102
+ c.get("feishu_doc_url", "") or c.get("url", "") or "",
103
+ ]
104
+ for c in contexts
105
+ ],
106
+ title=title,
107
+ )
108
+
109
+
110
+ def _dump_contexts(contexts: list[dict]):
111
+ """Fetch full content for each context and output as markdown."""
112
+ for c in contexts:
113
+ title = c.get("title", "Untitled")
114
+ ctx_type = c.get("type", "")
115
+ print(f"## {title}\n")
116
+
117
+ if ctx_type == "feishu_doc" and c.get("feishu_doc_token"):
118
+ try:
119
+ content = _fetch_feishu_doc(c["feishu_doc_token"])
120
+ print(content)
121
+ except Exception as e:
122
+ print(f"[Failed to fetch document: {e}]")
123
+ elif c.get("content"):
124
+ print(c["content"])
125
+ elif c.get("url"):
126
+ print(f"Link: {c['url']}")
127
+ elif c.get("feishu_doc_url"):
128
+ print(f"Link: {c['feishu_doc_url']}")
129
+ else:
130
+ print(f"[{ctx_type} context, no inline content]")
131
+
132
+ print()
133
+
134
+
135
+ # ── Issue contexts ──────────────────────────────────────────────
136
+
137
+
138
+ @app.command("list")
139
+ def list_contexts(
140
+ seq_num: int = typer.Argument(..., help="Issue sequence number"),
141
+ workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace slug (overrides default)"),
142
+ format: str = typer.Option("table", "--format", "-f", help="Output format: table or json"),
143
+ ):
144
+ """List contexts for an issue."""
145
+ slug = _get_workspace(workspace)
146
+ try:
147
+ contexts = asyncio.run(_list_issue_contexts(slug, seq_num))
148
+ except Exception as e:
149
+ console.print(f"[red]Error:[/red] {e}")
150
+ raise typer.Exit(1)
151
+ _print_contexts(contexts, f"Contexts for issue #{seq_num}", format)
152
+
153
+
154
+ @app.command("dump")
155
+ def dump_issue_contexts(
156
+ seq_num: int = typer.Argument(..., help="Issue sequence number"),
157
+ workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace slug (overrides default)"),
158
+ ):
159
+ """Dump all context content for an issue. Fetches feishu doc content inline."""
160
+ slug = _get_workspace(workspace)
161
+ try:
162
+ contexts = asyncio.run(_list_issue_contexts(slug, seq_num))
163
+ except Exception as e:
164
+ console.print(f"[red]Error:[/red] {e}", stderr=True)
165
+ raise typer.Exit(1)
166
+ if not contexts:
167
+ console.print("No contexts found.", stderr=True)
168
+ raise typer.Exit(1)
169
+ _dump_contexts(contexts)
170
+
171
+
172
+ # ── Space contexts ──────────────────────────────────────────────
173
+
174
+
175
+ @app.command("list-space")
176
+ def list_space_contexts(
177
+ slug: str = typer.Argument(..., help="Workspace slug"),
178
+ format: str = typer.Option("table", "--format", "-f", help="Output format: table or json"),
179
+ ):
180
+ """List contexts for a workspace (space-level)."""
181
+ try:
182
+ contexts = asyncio.run(_list_space_contexts(slug))
183
+ except Exception as e:
184
+ console.print(f"[red]Error:[/red] {e}")
185
+ raise typer.Exit(1)
186
+ _print_contexts(contexts, f"Space contexts for {slug}", format)
187
+
188
+
189
+ @app.command("dump-space")
190
+ def dump_space_contexts(
191
+ slug: str = typer.Argument(..., help="Workspace slug"),
192
+ ):
193
+ """Dump all context content for a workspace. Fetches feishu doc content inline."""
194
+ try:
195
+ contexts = asyncio.run(_list_space_contexts(slug))
196
+ except Exception as e:
197
+ console.print(f"[red]Error:[/red] {e}", stderr=True)
198
+ raise typer.Exit(1)
199
+ if not contexts:
200
+ console.print("No contexts found.", stderr=True)
201
+ raise typer.Exit(1)
202
+ _dump_contexts(contexts)
203
+
204
+
205
+ # ── Placeholder commands ────────────────────────────────────────
206
+
207
+
208
+ @app.command("add-doc")
209
+ def add_doc(
210
+ seq_num: int = typer.Argument(..., help="Issue sequence number"),
211
+ workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace slug"),
212
+ title: str = typer.Option(..., "--title", "-t", help="Document title"),
213
+ content: Optional[str] = typer.Option(None, "--content", "-c", help="Markdown content"),
214
+ content_file: Optional[str] = typer.Option(None, "--content-file", help="Read content from file (use - for stdin)"),
215
+ ):
216
+ """Create a new Feishu doc with content and link it to an issue."""
217
+ slug = _get_workspace(workspace)
218
+
219
+ # Resolve content
220
+ doc_content = content
221
+ if content_file:
222
+ if content_file == "-":
223
+ doc_content = sys.stdin.read()
224
+ else:
225
+ doc_content = Path(content_file).read_text(encoding="utf-8")
226
+
227
+ if not doc_content:
228
+ console.print("[red]Error:[/red] Provide --content or --content-file")
229
+ raise typer.Exit(1)
230
+
231
+ try:
232
+ # Step 1: Create feishu doc via lark-cli
233
+ console.print(f"Creating feishu doc: {title} ...")
234
+ doc_token, doc_url = _create_feishu_doc(title)
235
+
236
+ # Step 2: Write content via lark-cli
237
+ console.print("Writing content ...")
238
+ _update_feishu_doc(doc_token, doc_content)
239
+
240
+ # Step 3: Link to issue via pmflow API
241
+ async def _link():
242
+ async with PmflowClient() as client:
243
+ resp = await client.http.post(
244
+ f"/workspaces/{slug}/issues/{seq_num}/contexts/feishu",
245
+ json={
246
+ "title": title,
247
+ "feishu_doc_token": doc_token,
248
+ "feishu_doc_url": doc_url,
249
+ "create_new": False,
250
+ },
251
+ )
252
+ resp.raise_for_status()
253
+ return resp.json()
254
+
255
+ result = asyncio.run(_link())
256
+ console.print(f"[green]Done![/green] {result.get('title', '')} -> {doc_url}")
257
+ except Exception as e:
258
+ console.print(f"[red]Error:[/red] {e}")
259
+ raise typer.Exit(1)
260
+
261
+
262
+ @app.command()
263
+ def show(ctx_id: str):
264
+ """Show context details."""
265
+ raise NotImplementedError
266
+
267
+
268
+ @app.command()
269
+ def remove(ctx_id: str):
270
+ """Remove a context."""
271
+ raise NotImplementedError
@@ -0,0 +1,48 @@
1
+ import typer
2
+
3
+ from pmflow_shared.enums import IssueStatus, IssueType
4
+
5
+ app = typer.Typer(name="issue", help="Issue management")
6
+
7
+
8
+ @app.command("list")
9
+ def list_issues(
10
+ status: IssueStatus | None = None,
11
+ type: IssueType | None = None,
12
+ ):
13
+ """List issues in current workspace."""
14
+ raise NotImplementedError
15
+
16
+
17
+ @app.command()
18
+ def create(
19
+ title: str,
20
+ type: IssueType = IssueType.TASK,
21
+ priority: int = 0,
22
+ ):
23
+ """Create a new issue."""
24
+ raise NotImplementedError
25
+
26
+
27
+ @app.command()
28
+ def show(seq_num: int):
29
+ """Show issue details."""
30
+ raise NotImplementedError
31
+
32
+
33
+ @app.command()
34
+ def update(seq_num: int):
35
+ """Update an issue."""
36
+ raise NotImplementedError
37
+
38
+
39
+ @app.command("status")
40
+ def set_status(seq_num: int, status: IssueStatus):
41
+ """Change issue status."""
42
+ raise NotImplementedError
43
+
44
+
45
+ @app.command()
46
+ def delete(seq_num: int):
47
+ """Delete an issue."""
48
+ raise NotImplementedError
@@ -0,0 +1,85 @@
1
+ """Install/uninstall pmflow Claude Code skills to ~/.claude/skills/."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from rich.table import Table
8
+
9
+ from pmflow_cli.output import console
10
+
11
+ app = typer.Typer(help="Manage Claude Code skills")
12
+
13
+ SKILLS_SOURCE = Path(__file__).parent.parent / "skills"
14
+ SKILLS_TARGET = Path.home() / ".claude" / "skills"
15
+ SKILL_NAMES = ["plan-space", "plan-task", "prdfix"]
16
+
17
+
18
+ def _prefixed(name: str) -> str:
19
+ return f"pmflow-{name}"
20
+
21
+
22
+ @app.command()
23
+ def install(
24
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing skills"),
25
+ ):
26
+ """Install pmflow skills into ~/.claude/skills/ for Claude Code."""
27
+ if not SKILLS_SOURCE.exists():
28
+ console.print("[red]Error:[/red] Skills source directory not found. Package may be corrupted.")
29
+ raise typer.Exit(1)
30
+
31
+ installed = []
32
+ skipped = []
33
+
34
+ for name in SKILL_NAMES:
35
+ src = SKILLS_SOURCE / name
36
+ dst = SKILLS_TARGET / _prefixed(name)
37
+
38
+ if dst.exists() and not force:
39
+ skipped.append(name)
40
+ continue
41
+
42
+ dst.mkdir(parents=True, exist_ok=True)
43
+ shutil.copy2(src / "SKILL.md", dst / "SKILL.md")
44
+ installed.append(name)
45
+
46
+ if installed:
47
+ console.print(f"[green]Installed {len(installed)} skill(s):[/green] {', '.join(installed)}")
48
+ if skipped:
49
+ console.print(f"[yellow]Skipped {len(skipped)} (already exist):[/yellow] {', '.join(skipped)}")
50
+ console.print("Use [bold]--force[/bold] to overwrite.")
51
+
52
+ if installed:
53
+ console.print("\n[dim]Skills are now available as /pmflow:plan-space, /pmflow:plan-task, /pmflow:prdfix in Claude Code.[/dim]")
54
+
55
+
56
+ @app.command()
57
+ def uninstall():
58
+ """Remove pmflow skills from ~/.claude/skills/."""
59
+ removed = []
60
+ for name in SKILL_NAMES:
61
+ dst = SKILLS_TARGET / _prefixed(name)
62
+ if dst.exists():
63
+ shutil.rmtree(dst)
64
+ removed.append(name)
65
+
66
+ if removed:
67
+ console.print(f"[green]Removed {len(removed)} skill(s):[/green] {', '.join(removed)}")
68
+ else:
69
+ console.print("[dim]No pmflow skills found to remove.[/dim]")
70
+
71
+
72
+ @app.command(name="list")
73
+ def list_skills():
74
+ """List pmflow skills and their install status."""
75
+ table = Table(title="pmflow Claude Code Skills")
76
+ table.add_column("Skill", style="bold")
77
+ table.add_column("Command")
78
+ table.add_column("Status")
79
+
80
+ for name in SKILL_NAMES:
81
+ dst = SKILLS_TARGET / _prefixed(name)
82
+ status = "[green]installed[/green]" if dst.exists() else "[dim]not installed[/dim]"
83
+ table.add_row(name, f"/pmflow:{name}", status)
84
+
85
+ console.print(table)
@@ -0,0 +1,63 @@
1
+ import asyncio
2
+
3
+ import typer
4
+
5
+ from pmflow_cli.client import PmflowClient
6
+ from pmflow_cli.config import CliConfig
7
+ from pmflow_cli.output import console, print_table
8
+
9
+ app = typer.Typer(name="ws", help="Workspace management")
10
+
11
+
12
+ @app.command("list")
13
+ def list_workspaces():
14
+ """List all workspaces."""
15
+
16
+ async def _list():
17
+ async with PmflowClient() as client:
18
+ resp = await client.http.get("/workspaces")
19
+ resp.raise_for_status()
20
+ return resp.json()
21
+
22
+ try:
23
+ workspaces = asyncio.run(_list())
24
+ except Exception as e:
25
+ console.print(f"[red]Error:[/red] {e}")
26
+ raise typer.Exit(1)
27
+
28
+ config = CliConfig.load()
29
+ print_table(
30
+ columns=["Slug", "Name", "Active"],
31
+ rows=[
32
+ [w.get("slug", ""), w.get("name", ""), "*" if w.get("slug") == config.workspace else ""]
33
+ for w in workspaces
34
+ ],
35
+ title="Workspaces",
36
+ )
37
+
38
+
39
+ @app.command()
40
+ def switch(slug: str):
41
+ """Switch active workspace."""
42
+ config = CliConfig.load()
43
+ config.workspace = slug
44
+ config.save()
45
+ console.print(f"[green]Switched to workspace:[/green] {slug}")
46
+
47
+
48
+ @app.command()
49
+ def create(name: str):
50
+ """Create a new workspace."""
51
+ raise NotImplementedError
52
+
53
+
54
+ @app.command()
55
+ def show(slug: str | None = None):
56
+ """Show workspace details."""
57
+ raise NotImplementedError
58
+
59
+
60
+ @app.command()
61
+ def delete(slug: str):
62
+ """Delete a workspace."""
63
+ raise NotImplementedError
pmflow_cli/config.py ADDED
@@ -0,0 +1,42 @@
1
+ from pathlib import Path
2
+ from dataclasses import dataclass
3
+
4
+ try:
5
+ import tomllib
6
+ except ImportError:
7
+ import tomli as tomllib
8
+
9
+ import tomli_w
10
+
11
+ CONFIG_DIR = Path.home() / ".pmflow"
12
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
13
+
14
+
15
+ @dataclass
16
+ class CliConfig:
17
+ server_url: str = "http://localhost:8000"
18
+ api_token: str = ""
19
+ workspace: str = ""
20
+ output_format: str = "table"
21
+
22
+ @classmethod
23
+ def load(cls) -> "CliConfig":
24
+ if not CONFIG_FILE.exists():
25
+ return cls()
26
+ data = tomllib.loads(CONFIG_FILE.read_text())
27
+ auth = data.get("auth", {})
28
+ defaults = data.get("defaults", {})
29
+ return cls(
30
+ server_url=auth.get("server_url", cls.server_url),
31
+ api_token=auth.get("api_token", cls.api_token),
32
+ workspace=defaults.get("workspace", cls.workspace),
33
+ output_format=defaults.get("output_format", cls.output_format),
34
+ )
35
+
36
+ def save(self) -> None:
37
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
38
+ data = {
39
+ "auth": {"server_url": self.server_url, "api_token": self.api_token},
40
+ "defaults": {"workspace": self.workspace, "output_format": self.output_format},
41
+ }
42
+ CONFIG_FILE.write_text(tomli_w.dumps(data))
pmflow_cli/main.py ADDED
@@ -0,0 +1,65 @@
1
+ import typer
2
+
3
+ from pmflow_cli.commands import auth, workspace, issue, context, ai, skills
4
+
5
+ app = typer.Typer(name="pmflow", help="pmflow - Project management for AI-assisted development")
6
+
7
+ app.add_typer(auth.app, name="auth")
8
+ app.add_typer(workspace.app, name="ws", help="Workspace management")
9
+ app.add_typer(issue.app, name="issue", help="Issue management")
10
+ app.add_typer(context.app, name="ctx", help="Context (attachments/docs) management")
11
+ app.add_typer(ai.app, name="ai", help="AI-assisted PRD generation")
12
+ app.add_typer(skills.app, name="skills", help="Manage Claude Code skills")
13
+
14
+
15
+ @app.command()
16
+ def login(
17
+ server: str = typer.Option("http://localhost:8000", "--server", "-s", prompt="Server URL"),
18
+ token: str = typer.Option(..., "--token", "-t", prompt="API Token (pmf_...)"),
19
+ ):
20
+ """Configure server URL and API token."""
21
+ from pmflow_cli.config import CliConfig
22
+ from pmflow_cli.output import console
23
+
24
+ config = CliConfig.load()
25
+ config.server_url = server
26
+ config.api_token = token
27
+ config.save()
28
+ console.print(f"[green]Saved.[/green] Server: {server}")
29
+
30
+
31
+ @app.command()
32
+ def logout():
33
+ """Clear local credentials."""
34
+ from pmflow_cli.config import CliConfig
35
+ from pmflow_cli.output import console
36
+
37
+ config = CliConfig.load()
38
+ config.api_token = ""
39
+ config.save()
40
+ console.print("[green]Token cleared.[/green]")
41
+
42
+
43
+ @app.command()
44
+ def whoami():
45
+ """Show current user info."""
46
+ import asyncio
47
+ from pmflow_cli.client import PmflowClient
48
+ from pmflow_cli.output import console
49
+
50
+ async def _whoami():
51
+ async with PmflowClient() as client:
52
+ resp = await client.http.get("/auth/me")
53
+ resp.raise_for_status()
54
+ return resp.json()
55
+
56
+ try:
57
+ user = asyncio.run(_whoami())
58
+ console.print(f"User: {user.get('username', '')} ({user.get('email', '')})")
59
+ except Exception as e:
60
+ console.print(f"[red]Error:[/red] {e}")
61
+ raise typer.Exit(1)
62
+
63
+
64
+ if __name__ == "__main__":
65
+ app()
pmflow_cli/output.py ADDED
@@ -0,0 +1,15 @@
1
+ """Rich terminal output formatting utilities."""
2
+
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+
6
+ console = Console()
7
+
8
+
9
+ def print_table(columns: list[str], rows: list[list[str]], title: str | None = None) -> None:
10
+ table = Table(title=title)
11
+ for col in columns:
12
+ table.add_column(col)
13
+ for row in rows:
14
+ table.add_row(*row)
15
+ console.print(table)
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: pmflow:plan-space
3
+ description: 获取 pmflow workspace 的所有上下文(PRD、设计文档等),进入 plan 模式制定技术实现方案。用法:/pmflow:plan-space {slug}
4
+ user-invocable: true
5
+ ---
6
+
7
+ 从参数 `$ARGUMENTS` 中获取 workspace slug。
8
+
9
+ 执行以下步骤:
10
+
11
+ ## 1. 获取 Space 的所有上下文内容
12
+
13
+ 运行命令:
14
+
15
+ ```
16
+ pmflow ctx dump-space {slug}
17
+ ```
18
+
19
+ 如果命令失败,将错误信息告知用户并停止。
20
+
21
+ ## 2. 进入 Plan 模式
22
+
23
+ 成功获取上下文内容后,进入 plan 模式。基于上下文中的需求文档(PRD、设计文档等),制定详细的技术实现方案:
24
+
25
+ - 仔细分析文档中的功能需求
26
+ - 根据本项目的架构确定需要创建或修改的文件
27
+ - 将工作拆解为有序的实现步骤
28
+ - 复用项目中已有的工具函数和模式
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: pmflow:plan-task
3
+ description: 获取 pmflow issue 的上下文并制定实现方案。用法:/pmflow:plan-task {slug}-{seq_num}(如 WS-3)
4
+ user-invocable: true
5
+ ---
6
+
7
+ 解析参数 `$ARGUMENTS`,格式为 `{slug}-{seq_num}`(如 `WS-3`),以最后一个 `-` 为分隔符拆分出 workspace slug(转小写)和 issue 序号。
8
+
9
+ 执行以下步骤:
10
+
11
+ ## 1. 获取 Issue 的所有上下文内容
12
+
13
+ 运行命令:
14
+
15
+ ```
16
+ pmflow ctx dump {seq_num} --workspace {slug}
17
+ ```
18
+
19
+ 如果命令失败,将错误信息告知用户并停止。
20
+
21
+ ## 2. 进入 Plan 模式
22
+
23
+ 成功获取上下文内容后,进入 plan 模式。基于上下文中的需求文档(PRD、设计文档等),制定详细的技术实现方案:
24
+
25
+ - 仔细分析文档中的功能需求
26
+ - 根据本项目的架构确定需要创建或修改的文件
27
+ - 将工作拆解为有序的实现步骤
28
+ - 复用项目中已有的工具函数和模式
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: pmflow:prdfix
3
+ description: 回顾编码工作,生成 PRD 实现总结并创建飞书文档关联到 issue。用法:/pmflow:prdfix {slug}-{seq_num}(如 WS-3)
4
+ user-invocable: true
5
+ ---
6
+
7
+ 解析参数 `$ARGUMENTS`,格式为 `{slug}-{seq_num}`(如 `WS-3`),以最后一个 `-` 为分隔符拆分出 workspace slug(转小写)和 issue 序号。
8
+
9
+ 执行以下步骤:
10
+
11
+ ## 1. 回顾实际实现
12
+
13
+ 回顾本次对话中的所有编码工作,总结**实际实现方案**。注意:
14
+ - 只总结思路和方案,不要包含具体代码
15
+ - 重点关注与原始 PRD 的差异(如有)
16
+ - 记录关键设计决策和取舍
17
+ - 包括实现过程中发现的问题和解决方式
18
+
19
+ 用 Markdown 格式组织总结,包含以下部分:
20
+ - **实现概述**:一段话概括实际做了什么
21
+ - **与原始 PRD 的差异**:列出偏离原计划的地方及原因(如完全一致则注明)
22
+ - **关键设计决策**:重要的技术选型和权衡
23
+ - **已知局限 / 后续改进**:当前方案的已知限制
24
+
25
+ ## 2. 创建飞书文档并关联 Issue
26
+
27
+ 将上述总结内容写入临时文件 `/tmp/prdfix-summary.md`,然后运行命令:
28
+
29
+ ```
30
+ pmflow ctx add-doc {seq_num} --workspace {slug} --title "PRD 实现总结 - {slug上大写}-{seq_num}" --content-file /tmp/prdfix-summary.md
31
+ ```
32
+
33
+ 如果命令失败,将错误信息告知用户并停止。
34
+
35
+ ## 3. 确认完成
36
+
37
+ 命令成功后,将飞书文档链接展示给用户,确认总结已保存到对应 Issue 的上下文中。
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: pmflow-cli
3
+ Version: 0.1.0
4
+ Summary: CLI for pmflow — project management for AI-assisted development
5
+ Project-URL: Repository, https://github.com/zhanglaixian/pmflow
6
+ Author-email: zhanglaixian <zhanglaixian@zhuanzhuan.com>
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.12
13
+ Requires-Dist: httpx>=0.27
14
+ Requires-Dist: pmflow-shared>=0.1.0
15
+ Requires-Dist: rich>=13.0
16
+ Requires-Dist: tomli-w>=1.0
17
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
18
+ Requires-Dist: typer>=0.12
19
+ Description-Content-Type: text/markdown
20
+
21
+ # pmflow
22
+
23
+ CLI for pmflow — an integrated project management and AI-assisted development platform.
24
+
25
+ pmflow connects project planning (PRDs, design docs) with AI coding tools, providing seamless context access via CLI and Claude Code slash commands.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install pmflow-cli
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ```bash
36
+ # Configure server connection
37
+ pmflow login --server https://your-server.com --token pmf_xxx
38
+
39
+ # Install Claude Code skills (optional)
40
+ pmflow skills install
41
+
42
+ # Basic commands
43
+ pmflow whoami # Check connection
44
+ pmflow ws list # List workspaces
45
+ pmflow issue list -w my-project # List issues
46
+ pmflow ctx dump 3 -w my-project # Dump issue context for AI
47
+ ```
48
+
49
+ ## Claude Code Integration
50
+
51
+ After running `pmflow skills install`, the following slash commands are available in Claude Code:
52
+
53
+ - `/pmflow:plan-space {slug}` — Load workspace context and plan implementation
54
+ - `/pmflow:plan-task {slug}-{seq}` — Load issue context and plan implementation
55
+ - `/pmflow:prdfix {slug}-{seq}` — Summarize implementation and create Feishu doc
@@ -0,0 +1,19 @@
1
+ pmflow_cli/__init__.py,sha256=HGg80_rzw39gOGkee-EZKlOhxI5Ntdj0wJwVjMDz4UA,46
2
+ pmflow_cli/client.py,sha256=ULZxn94Uq358Eq9UNIaAmDNSRW_XxiC_Vaqday_oWXs,572
3
+ pmflow_cli/config.py,sha256=O_LJyusBhpxb3g89pG3r5eV2oCIK4fvk6JqMa3Pg6Dc,1261
4
+ pmflow_cli/main.py,sha256=EuWSkFyVFMWGx4-5rRGBMhibe55XIZD4QygcdPTngCI,2016
5
+ pmflow_cli/output.py,sha256=3CcpMipMBCf-OAbJ97I8GiHOR6TVp_3cNQ3RZb6BU2k,388
6
+ pmflow_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ pmflow_cli/commands/ai.py,sha256=jnrYsVzwN0xMmkAb8_jK7VomSbOwhdvBHEK-USldzOI,328
8
+ pmflow_cli/commands/auth.py,sha256=7VNttGtTDZNNv3U3sJ9M-REgajRrOj6bB0G3TNf0XF4,166
9
+ pmflow_cli/commands/context.py,sha256=EsLWT2nNGc6GT4vIalktRDuxOHFacu-fQ4AnVEtOTBI,9584
10
+ pmflow_cli/commands/issue.py,sha256=WvM3ESi1rnNssOHVdAeK-tTMqQ9NzCeWsIfgodjFxFg,925
11
+ pmflow_cli/commands/skills.py,sha256=OOgVNi4DcUpPfnR2qo7V0HRspNCqiHIjbO3RYxmJi4U,2642
12
+ pmflow_cli/commands/workspace.py,sha256=21fdj5-JUQr-q5T6BNDh9yLebjexqhLKedOOLJ0Kk-8,1470
13
+ pmflow_cli/skills/plan-space/SKILL.md,sha256=5uZqe1q5KjRGFd2nHMCXBzWMdH_Jx_AMOyS1cqW8Dv0,829
14
+ pmflow_cli/skills/plan-task/SKILL.md,sha256=fWUGCm3bW_W9Tcfg1hhouzntXiryozM2T8HEGlzX9eo,921
15
+ pmflow_cli/skills/prdfix/SKILL.md,sha256=sYUpatW3cxXqH5SyWNYk7Nn-zcSaZH39y2iHrgsD-cY,1510
16
+ pmflow_cli-0.1.0.dist-info/METADATA,sha256=Ohd3FXDpx2FNK_BFumuiwNEFmDb3Jarhf7KPt6X9yDw,1802
17
+ pmflow_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
18
+ pmflow_cli-0.1.0.dist-info/entry_points.txt,sha256=DkjvACB0gIe9pd_c15o233T2QUbVsiBGLEnx-5vaB7c,47
19
+ pmflow_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pmflow = pmflow_cli.main:app