pm-skill 1.1.4 → 1.1.6

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/.env.example CHANGED
@@ -1,17 +1,25 @@
1
1
  # === Linear ===
2
- # Settings > API > Personal API Keys 에서 발급
2
+ # Get from: Linear > Settings > API > Personal API Keys
3
+ # 발급: Linear > Settings > API > Personal API Keys
3
4
  LINEAR_API_KEY=lin_api_xxxxxxxx
4
5
 
5
- # setup 커맨드로 확인 가능
6
+ # Discovered by `npx pm-skill init` (auto-detected)
7
+ # `npx pm-skill init`으로 자동 감지됨
6
8
  LINEAR_DEFAULT_TEAM_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
9
+
10
+ # Optional — discovered by `npx pm-skill init` or `select-project`
11
+ # 선택사항 — init 또는 select-project로 자동 감지
7
12
  LINEAR_DEFAULT_PROJECT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
8
13
 
9
- # === Notion ===
10
- # https://www.notion.so/my-integrations 에서 발급
14
+ # === Notion (optional) ===
15
+ # Get from: https://www.notion.so/my-integrations > New integration
16
+ # 발급: https://www.notion.so/my-integrations > New integration
11
17
  NOTION_API_KEY=secret_xxxxxxxx
12
18
 
13
- # Notion에서 문서를 생성할 상위 페이지 ID
19
+ # Parent page where docs are created — discovered by `npx pm-skill init`
20
+ # 문서가 생성될 상위 페이지 — init으로 자동 감지
14
21
  NOTION_ROOT_PAGE_ID=xxxxxxxx
15
22
 
16
- # (선택) 버그 추적 DB ID 설정 report-bug가 DB 엔트리로 생성
23
+ # Optional bug tracking DB ID (if omitted, bugs use pages instead)
24
+ # 선택사항 — 버그 추적 DB ID (미설정 시 페이지로 생성)
17
25
  NOTION_BUG_DB_ID=xxxxxxxx
package/AGENTS.md CHANGED
@@ -6,6 +6,38 @@ This file provides instructions for AI coding assistants (Codex, Claude Code, et
6
6
 
7
7
  A structured project management CLI that integrates Linear (issue tracking) and Notion (documentation). It enforces a config-driven workflow where only pre-defined labels, templates, and severity levels are allowed.
8
8
 
9
+ ## Rules
10
+
11
+ - If the user writes `/pm-skill <args>`, execute `npx pm-skill <args>` from the project root.
12
+ - When the user mentions pm-skill, Linear issues, or Notion documents, prefer using pm-skill commands.
13
+ - This tool requires shell execution and network access (Linear/Notion APIs).
14
+
15
+ ## Prerequisites
16
+
17
+ - Node.js 18 or higher (`node --version` to check)
18
+ - Linear API key (get from: Linear > Settings > API > Personal API Keys)
19
+ - Notion API key (optional, get from: https://www.notion.so/my-integrations)
20
+
21
+ ## First-Time Setup
22
+
23
+ If `.env` does not exist in the project root, run initialization first:
24
+
25
+ ```bash
26
+ # Step 1: Initialize (validates keys, creates .env, config.yml, SKILL.md, AGENTS.md)
27
+ npx pm-skill init --linear-key <LINEAR_API_KEY> --notion-key <NOTION_API_KEY>
28
+
29
+ # Step 2: Verify setup and label matching
30
+ npx pm-skill setup
31
+
32
+ # Step 3 (optional): Create missing labels in Linear
33
+ npx pm-skill setup --sync
34
+ ```
35
+
36
+ If init fails:
37
+ - **"Linear API key validation failed"** — check the key at Linear > Settings > API
38
+ - **"No pages shared"** — share a Notion page with the integration first (page menu > Connections)
39
+ - **Network error** — check internet connection and retry
40
+
9
41
  ## How to Run Commands
10
42
 
11
43
  ```bash
@@ -21,8 +53,22 @@ npx pm-skill setup
21
53
  npx pm-skill setup --sync # create missing labels in Linear
22
54
  ```
23
55
 
56
+ ### select-project
57
+ List or switch the active Linear project.
58
+ ```bash
59
+ npx pm-skill select-project # list projects
60
+ npx pm-skill select-project "Project Name" # switch
61
+ ```
62
+
63
+ ### select-page
64
+ List or switch the active Notion root page.
65
+ ```bash
66
+ npx pm-skill select-page # list pages
67
+ npx pm-skill select-page "Page Name" # switch
68
+ ```
69
+
24
70
  ### start-feature
25
- Create a Linear issue + Notion PRD page with bidirectional links.
71
+ Create a Linear issue with a task checklist.
26
72
  ```bash
27
73
  npx pm-skill start-feature "<title>"
28
74
  ```
@@ -45,7 +91,6 @@ Set a relationship between two issues.
45
91
  ```bash
46
92
  npx pm-skill relate <issue1> <issue2> --type <related|similar>
47
93
  ```
48
- Default type: related.
49
94
 
50
95
  ### block
51
96
  Set a blocking dependency (issue1 blocks issue2).
@@ -53,6 +98,30 @@ Set a blocking dependency (issue1 blocks issue2).
53
98
  npx pm-skill block <blocker-issue> <blocked-issue>
54
99
  ```
55
100
 
101
+ ### push-doc
102
+ Upload markdown to Notion. Optionally link to a Linear issue.
103
+ ```bash
104
+ # From file
105
+ npx pm-skill push-doc ./doc.md --title "Title" --parent <page-id> --issue <issue-id>
106
+
107
+ # From content (AI agent use case)
108
+ npx pm-skill push-doc --title "Title" --content "# Markdown..." --issue <issue-id>
109
+ ```
110
+
111
+ ### update-doc
112
+ Replace existing Notion page content with new markdown.
113
+ ```bash
114
+ npx pm-skill update-doc <page-id> ./updated.md
115
+ npx pm-skill update-doc <page-id> --content "# Updated..."
116
+ ```
117
+
118
+ ### create-folder
119
+ Create an empty Notion page as a category/folder.
120
+ ```bash
121
+ npx pm-skill create-folder "Folder Name" --parent <page-id>
122
+ # Returns page ID — use with push-doc --parent
123
+ ```
124
+
56
125
  ### attach-doc
57
126
  Attach a document URL to an issue with type validation.
58
127
  ```bash
@@ -65,21 +134,33 @@ Show issue details including children, relations, and attachments.
65
134
  npx pm-skill get <issue-id>
66
135
  ```
67
136
 
137
+ ### delete
138
+ Delete issue(s) and their linked Notion pages.
139
+ ```bash
140
+ npx pm-skill delete <issue-id>
141
+ npx pm-skill delete <issue-id> --recursive # also delete sub-issues
142
+ ```
143
+
68
144
  ## Configuration
69
145
 
70
146
  All config is per-project (CWD):
71
- - **`.env`** — API keys and IDs
147
+ - **`.env`** — API keys and IDs (never commit this file)
72
148
  - **`config.yml`** — Labels, templates, priorities, severity mappings
73
149
 
74
150
  ## Workflow Patterns
75
151
 
76
152
  ### Feature Development
77
- 1. `npx pm-skill start-feature "Feature Name"` — creates Linear issue + Notion PRD
153
+ 1. `npx pm-skill start-feature "Feature Name"` — creates Linear issue with checklist
78
154
  2. `npx pm-skill add-task ENG-XX "Sub-task 1"` — break down into sub-tasks
79
- 3. `npx pm-skill relate ENG-XX ENG-YY` — link related issues
80
- 4. `npx pm-skill attach-doc ENG-XX --url "..." --title "..." --type source-of-truth`
155
+ 3. `npx pm-skill push-doc ./design.md --issue ENG-XX` — upload docs when ready
156
+ 4. `npx pm-skill relate ENG-XX ENG-YY` link related issues
81
157
 
82
158
  ### Bug Fix
83
159
  1. `npx pm-skill report-bug "Bug Description" --severity high`
84
160
  2. `npx pm-skill add-task ENG-XX "Root cause analysis"`
85
161
  3. `npx pm-skill add-task ENG-XX "Fix and test"`
162
+
163
+ ### Document Management
164
+ 1. `npx pm-skill create-folder "Schema Docs"` — create category
165
+ 2. `npx pm-skill push-doc ./schema.md --parent <folder-id>` — upload under category
166
+ 3. `npx pm-skill update-doc <page-id> ./schema-v2.md` — update later
package/README.md CHANGED
@@ -15,6 +15,12 @@ Structured project management CLI that integrates **Linear** and **Notion**. Des
15
15
  - **attach-doc** — Attach documents with type validation
16
16
  - **get** — View issue details with children, relations, and attachments
17
17
 
18
+ ## Prerequisites
19
+
20
+ - **Node.js 18+** — check with `node --version`
21
+ - **Linear API key** — Linear > Settings > API > Personal API Keys
22
+ - **Notion API key** (optional) — https://www.notion.so/my-integrations
23
+
18
24
  ## Quick Start
19
25
 
20
26
  ```bash
@@ -122,6 +128,10 @@ npx pm-skill start-feature "My Feature"
122
128
  | `doc_types` | Document types for attach-doc validation |
123
129
  | `epics` | Epic definitions (project-specific) |
124
130
 
131
+ ## Security
132
+
133
+ `.env` contains API keys — **never commit it to git**. The `init` command creates `.env` locally, and it's already in `.gitignore`.
134
+
125
135
  ## Per-Project Model
126
136
 
127
137
  All config is per-project. Each project gets its own `.env`, `config.yml`, and instruction files. Run `npx pm-skill init` in each project directory.
package/SKILL.md CHANGED
@@ -1,8 +1,24 @@
1
+ ---
2
+ name: pm-skill
3
+ description: Structured project management CLI — Linear + Notion integration. Trigger on: pm-skill, Linear issue, Notion doc, start-feature, report-bug, push-doc, backlog
4
+ ---
5
+
1
6
  # PM Skill — Structured Project Management
2
7
 
3
8
  Linear + Notion integration for structured project management.
4
9
  "Design freedom, usage discipline" — only labels/templates/severity defined in `config.yml` are allowed.
5
10
 
11
+ ## Rules
12
+
13
+ - When the user mentions **pm-skill**, **Linear issue**, **Notion document**, **start-feature**, **report-bug**, **backlog**, or **push-doc**, use this skill.
14
+ - If the user writes `/pm-skill <args>`, execute `npx pm-skill <args>` from the project root.
15
+ - This skill requires **shell execution** and **network access** (Linear/Notion APIs).
16
+ - If `.env` does not exist, run `npx pm-skill init` first.
17
+
18
+ ## Prerequisites
19
+
20
+ Requires Node.js 18+. If `.env` does not exist in the project, run `init` first.
21
+
6
22
  ## Setup
7
23
 
8
24
  ```bash
@@ -14,6 +30,9 @@ npx pm-skill setup
14
30
 
15
31
  # Create missing labels in Linear
16
32
  npx pm-skill setup --sync
33
+
34
+ # Install as Codex global skill (optional)
35
+ npx pm-skill install-codex-skill
17
36
  ```
18
37
 
19
38
  ## Commands
@@ -24,8 +43,15 @@ Verify Linear/Notion connection + label matching. `--sync` creates missing label
24
43
  npx pm-skill setup
25
44
  ```
26
45
 
46
+ ### select-project / select-page
47
+ Switch active Linear project or Notion root page.
48
+ ```bash
49
+ npx pm-skill select-project "Project Name"
50
+ npx pm-skill select-page "Page Name"
51
+ ```
52
+
27
53
  ### start-feature
28
- Start feature development. Creates Linear issue + Notion PRD + bidirectional links.
54
+ Start feature development. Creates Linear issue with task checklist.
29
55
  ```bash
30
56
  npx pm-skill start-feature "Feature title"
31
57
  ```
@@ -43,20 +69,41 @@ Add sub-task to an issue.
43
69
  npx pm-skill add-task ENG-10 "Write unit tests"
44
70
  ```
45
71
 
46
- ### relate
47
- Link two issues. (related, similar)
72
+ ### relate / block
73
+ Link or set blocking relationship between issues.
48
74
  ```bash
49
75
  npx pm-skill relate ENG-10 ENG-11 --type related
76
+ npx pm-skill block ENG-10 ENG-11
50
77
  ```
51
78
 
52
- ### block
53
- Set blocking relationship. (ENG-10 must complete before ENG-11)
79
+ ### push-doc
80
+ Upload markdown to Notion. Optionally link to a Linear issue.
54
81
  ```bash
55
- npx pm-skill block ENG-10 ENG-11
82
+ # From file
83
+ npx pm-skill push-doc ./design.md --title "Design Doc" --issue ENG-10
84
+
85
+ # From content (AI agent use case)
86
+ npx pm-skill push-doc --title "Report" --content "# Results..." --issue ENG-10
87
+
88
+ # Under a specific parent page
89
+ npx pm-skill push-doc ./schema.md --parent <page-id>
90
+ ```
91
+
92
+ ### update-doc
93
+ Replace existing Notion page content with new markdown.
94
+ ```bash
95
+ npx pm-skill update-doc <page-id> ./updated.md
96
+ npx pm-skill update-doc <page-id> --content "# Updated..."
97
+ ```
98
+
99
+ ### create-folder
100
+ Create Notion page as a category/folder.
101
+ ```bash
102
+ npx pm-skill create-folder "Schema Docs" --parent <page-id>
56
103
  ```
57
104
 
58
105
  ### attach-doc
59
- Attach document URL to issue. Type is validated against config.
106
+ Attach a document URL to an issue with type validation.
60
107
  ```bash
61
108
  npx pm-skill attach-doc ENG-10 \
62
109
  --url "https://notion.so/..." \
@@ -71,6 +118,13 @@ Show issue details including sub-issues, relations, and attachments.
71
118
  npx pm-skill get ENG-10
72
119
  ```
73
120
 
121
+ ### delete
122
+ Delete issue(s) and linked Notion pages.
123
+ ```bash
124
+ npx pm-skill delete ENG-10
125
+ npx pm-skill delete ENG-10 --recursive # also delete sub-issues
126
+ ```
127
+
74
128
  ## Workflow Examples
75
129
 
76
130
  ### Feature Development
@@ -78,9 +132,8 @@ npx pm-skill get ENG-10
78
132
  npx pm-skill start-feature "Booking cancellation"
79
133
  npx pm-skill add-task ENG-10 "API endpoint"
80
134
  npx pm-skill add-task ENG-10 "Frontend UI"
81
- npx pm-skill add-task ENG-10 "Tests"
135
+ npx pm-skill push-doc ./design.md --issue ENG-10
82
136
  npx pm-skill relate ENG-10 ENG-8 --type related
83
- npx pm-skill block ENG-10 ENG-15
84
137
  ```
85
138
 
86
139
  ### Bug Fix
@@ -90,13 +143,9 @@ npx pm-skill add-task ENG-20 "Root cause analysis"
90
143
  npx pm-skill add-task ENG-20 "Fix and test"
91
144
  ```
92
145
 
93
- ## Config
94
-
95
- | Section | Description |
96
- |---------|-------------|
97
- | `labels` | Available labels (description required) |
98
- | `templates` | Command → label/priority/Notion template mappings |
99
- | `priorities` | p0-p3 → Linear priority mapping |
100
- | `severity_mapping` | severity name → priority key |
101
- | `doc_types` | Document types for attach-doc |
102
- | `epics` | Epic definitions |
146
+ ### Document Management
147
+ ```bash
148
+ npx pm-skill create-folder "Schema Docs"
149
+ npx pm-skill push-doc ./schema.md --parent <folder-id>
150
+ npx pm-skill update-doc <page-id> ./schema-v2.md
151
+ ```
package/config.yml CHANGED
@@ -19,43 +19,29 @@ labels:
19
19
  name: Refactor
20
20
  description: "기존 코드 리팩토링 (기능 변경 없음)"
21
21
  color: "#888888"
22
- - id: admin
23
- name: Admin
24
- description: "인프라, 배포, 환경 설정 등 관리 작업"
25
- color: "#ffaa00"
26
- - id: guest
27
- name: Guest
28
- description: "외부 의존성, 서드파티 연동 관련"
29
- color: "#aa88ff"
30
-
31
22
  templates:
32
23
  - id: feature
33
- name: Feature PRD
34
- description: "기능 개발용 PRD 템플릿"
35
- notion_template: feature-prd
24
+ name: Feature
25
+ description: "기능 개발 템플릿"
36
26
  linear_labels: [dev]
37
27
  linear_priority: p2
38
28
  - id: bugfix
39
- name: Bug Report
40
- description: "버그 리포트 템플릿"
41
- notion_template: bug-report
29
+ name: Bug Fix
30
+ description: "버그 수정 템플릿"
42
31
  linear_labels: [bug]
43
32
  - id: improvement
44
33
  name: Improvement
45
34
  description: "기존 기능 개선 템플릿"
46
- notion_template: improvement
47
35
  linear_labels: [dev]
48
36
  linear_priority: p2
49
37
  - id: refactor
50
38
  name: Refactor
51
39
  description: "리팩토링 템플릿"
52
- notion_template: refactor
53
40
  linear_labels: [refactor]
54
41
  linear_priority: p3
55
42
  - id: design
56
- name: Design Doc
43
+ name: Design
57
44
  description: "설계 문서 템플릿"
58
- notion_template: design-doc
59
45
  linear_labels: [design, plan]
60
46
  linear_priority: p2
61
47
 
package/dist/config.d.ts CHANGED
@@ -8,7 +8,6 @@ export interface PmTemplate {
8
8
  id: string;
9
9
  name: string;
10
10
  description: string;
11
- notion_template: string;
12
11
  linear_labels: string[];
13
12
  linear_priority?: string;
14
13
  }
package/dist/config.js CHANGED
@@ -26,8 +26,8 @@ function validateTemplates(templates, labelIds) {
26
26
  throw new ConfigValidationError("templates array is empty or missing.");
27
27
  }
28
28
  for (const tmpl of templates) {
29
- if (!tmpl.id || !tmpl.name || !tmpl.description || !tmpl.notion_template) {
30
- throw new ConfigValidationError(`Template '${tmpl.id ?? "(unknown)"}' is missing required fields (id, name, description, notion_template).`);
29
+ if (!tmpl.id || !tmpl.name || !tmpl.description) {
30
+ throw new ConfigValidationError(`Template '${tmpl.id ?? "(unknown)"}' is missing required fields (id, name, description).`);
31
31
  }
32
32
  if (Array.isArray(tmpl.linear_labels)) {
33
33
  for (const lid of tmpl.linear_labels) {
package/dist/env.js CHANGED
@@ -20,7 +20,6 @@ export function resolveFile(filename) {
20
20
  return null;
21
21
  }
22
22
  const REQUIRED_KEYS = ["LINEAR_API_KEY", "LINEAR_DEFAULT_TEAM_ID"];
23
- const NOTION_KEYS = ["NOTION_API_KEY", "NOTION_ROOT_PAGE_ID"];
24
23
  const KEY_HELP = {
25
24
  LINEAR_API_KEY: "Linear > Settings > API > Personal API Keys",
26
25
  LINEAR_DEFAULT_TEAM_ID: "'npx pm-skill init' to discover your team ID",
@@ -66,13 +65,6 @@ export function validateEnv(command) {
66
65
  missing.push(key);
67
66
  }
68
67
  }
69
- const notionCommands = ["start-feature", "report-bug"];
70
- if (notionCommands.includes(command)) {
71
- for (const key of NOTION_KEYS) {
72
- if (!process.env[key])
73
- missing.push(key);
74
- }
75
- }
76
68
  if (missing.length > 0) {
77
69
  const hints = missing
78
70
  .map((k) => ` ${k}: ${KEY_HELP[k] ?? ""}`)
package/dist/notion.d.ts CHANGED
@@ -9,24 +9,6 @@ export declare function validateNotionKey(apiKey: string): Promise<{
9
9
  id: string;
10
10
  name: string;
11
11
  }>;
12
- export declare function buildFeaturePRD(title: string, linearUrl: string): BlockObjectRequest[];
13
- export declare function buildBugReport(title: string, severity: string, linearUrl: string): BlockObjectRequest[];
14
- export declare function buildDesignDoc(title: string, linearUrl: string): BlockObjectRequest[];
15
- export declare function buildImprovement(title: string, linearUrl: string): BlockObjectRequest[];
16
- export declare function buildRefactor(title: string, linearUrl: string): BlockObjectRequest[];
17
- export declare function getTemplateBlocks(notionTemplate: string, title: string, linearUrl: string, severity?: string): BlockObjectRequest[];
18
- export declare function createPage(client: Client, parentPageId: string, title: string, blocks: BlockObjectRequest[]): Promise<{
19
- id: string;
20
- url: string;
21
- }>;
22
- export declare function createDatabaseEntry(client: Client, databaseId: string, properties: Record<string, unknown>): Promise<{
23
- id: string;
24
- url: string;
25
- }>;
26
- export declare function createTemplatedPage(client: Client, parentPageId: string, notionTemplate: string, title: string, linearUrl: string, severity?: string): Promise<{
27
- id: string;
28
- url: string;
29
- }>;
30
12
  export declare function getPage(client: Client, pageId: string): Promise<Record<string, unknown>>;
31
13
  export declare function searchPages(client: Client, query: string): Promise<Array<{
32
14
  id: string;
package/dist/notion.js CHANGED
@@ -22,193 +22,7 @@ export async function validateNotionKey(apiKey) {
22
22
  throw new Error("Notion API key validation failed. Check your key at: https://www.notion.so/my-integrations");
23
23
  }
24
24
  }
25
- // ── Block Builders ──
26
- function text(content) {
27
- return [{ type: "text", text: { content } }];
28
- }
29
- function heading1(content) {
30
- return {
31
- object: "block",
32
- type: "heading_1",
33
- heading_1: { rich_text: text(content) },
34
- };
35
- }
36
- function heading2(content) {
37
- return {
38
- object: "block",
39
- type: "heading_2",
40
- heading_2: { rich_text: text(content) },
41
- };
42
- }
43
- function paragraph(content) {
44
- return {
45
- object: "block",
46
- type: "paragraph",
47
- paragraph: { rich_text: text(content) },
48
- };
49
- }
50
- function todo(content, checked = false) {
51
- return {
52
- object: "block",
53
- type: "to_do",
54
- to_do: { rich_text: text(content), checked },
55
- };
56
- }
57
- function numberedItem(content) {
58
- return {
59
- object: "block",
60
- type: "numbered_list_item",
61
- numbered_list_item: { rich_text: text(content) },
62
- };
63
- }
64
- function bookmark(url) {
65
- return {
66
- object: "block",
67
- type: "bookmark",
68
- bookmark: { url },
69
- };
70
- }
71
- function divider() {
72
- return {
73
- object: "block",
74
- type: "divider",
75
- divider: {},
76
- };
77
- }
78
- // ── Template Builders ──
79
- export function buildFeaturePRD(title, linearUrl) {
80
- return [
81
- heading2(`📋 ${title}`),
82
- bookmark(linearUrl),
83
- divider(),
84
- heading1("목표"),
85
- paragraph(""),
86
- heading1("배경"),
87
- paragraph(""),
88
- heading1("요구사항"),
89
- todo("요구사항 1"),
90
- todo("요구사항 2"),
91
- todo("요구사항 3"),
92
- heading1("설계"),
93
- paragraph(""),
94
- heading1("테스트 계획"),
95
- paragraph(""),
96
- ];
97
- }
98
- export function buildBugReport(title, severity, linearUrl) {
99
- return [
100
- heading2(`🐛 ${title} (${severity})`),
101
- bookmark(linearUrl),
102
- divider(),
103
- heading1("재현 단계"),
104
- numberedItem("단계 1"),
105
- numberedItem("단계 2"),
106
- numberedItem("단계 3"),
107
- heading1("예상 동작"),
108
- paragraph(""),
109
- heading1("실제 동작"),
110
- paragraph(""),
111
- heading1("환경"),
112
- paragraph("OS / 브라우저 / 디바이스"),
113
- heading1("해결 방안"),
114
- paragraph(""),
115
- ];
116
- }
117
- export function buildDesignDoc(title, linearUrl) {
118
- return [
119
- heading2(`📐 ${title}`),
120
- bookmark(linearUrl),
121
- divider(),
122
- heading1("개요"),
123
- paragraph(""),
124
- heading1("제약 조건"),
125
- paragraph(""),
126
- heading1("옵션 비교"),
127
- paragraph(""),
128
- heading1("결정"),
129
- paragraph(""),
130
- heading1("후속 작업"),
131
- todo("후속 작업 1"),
132
- todo("후속 작업 2"),
133
- ];
134
- }
135
- export function buildImprovement(title, linearUrl) {
136
- return [
137
- heading2(`✨ ${title}`),
138
- bookmark(linearUrl),
139
- divider(),
140
- heading1("현재 상태"),
141
- paragraph(""),
142
- heading1("개선 목표"),
143
- paragraph(""),
144
- heading1("변경 사항"),
145
- todo("변경 1"),
146
- todo("변경 2"),
147
- todo("변경 3"),
148
- heading1("영향 범위"),
149
- paragraph(""),
150
- ];
151
- }
152
- export function buildRefactor(title, linearUrl) {
153
- return [
154
- heading2(`🔧 ${title}`),
155
- bookmark(linearUrl),
156
- divider(),
157
- heading1("리팩토링 대상"),
158
- paragraph(""),
159
- heading1("현재 문제점"),
160
- paragraph(""),
161
- heading1("변경 계획"),
162
- todo("단계 1"),
163
- todo("단계 2"),
164
- heading1("검증 방법"),
165
- paragraph(""),
166
- ];
167
- }
168
- // ── Template Dispatcher ──
169
- const TEMPLATE_BUILDERS = {
170
- "feature-prd": buildFeaturePRD,
171
- "bug-report": (t, u, s) => buildBugReport(t, s ?? "medium", u),
172
- "design-doc": buildDesignDoc,
173
- improvement: buildImprovement,
174
- refactor: buildRefactor,
175
- };
176
- export function getTemplateBlocks(notionTemplate, title, linearUrl, severity) {
177
- const builder = TEMPLATE_BUILDERS[notionTemplate];
178
- if (!builder) {
179
- const available = Object.keys(TEMPLATE_BUILDERS).join(", ");
180
- throw new Error(`notion_template '${notionTemplate}'에 대한 빌더가 없습니다.\n등록된 템플릿: ${available}`);
181
- }
182
- return builder(title, linearUrl, severity);
183
- }
184
25
  // ── Page CRUD ──
185
- export async function createPage(client, parentPageId, title, blocks) {
186
- const response = await client.pages.create({
187
- parent: { page_id: parentPageId },
188
- properties: {
189
- title: { title: [{ text: { content: title } }] },
190
- },
191
- children: blocks,
192
- });
193
- return {
194
- id: response.id,
195
- url: `https://notion.so/${response.id.replace(/-/g, "")}`,
196
- };
197
- }
198
- export async function createDatabaseEntry(client, databaseId, properties) {
199
- const response = await client.pages.create({
200
- parent: { database_id: databaseId },
201
- properties: properties,
202
- });
203
- return {
204
- id: response.id,
205
- url: `https://notion.so/${response.id.replace(/-/g, "")}`,
206
- };
207
- }
208
- export async function createTemplatedPage(client, parentPageId, notionTemplate, title, linearUrl, severity) {
209
- const blocks = getTemplateBlocks(notionTemplate, title, linearUrl, severity);
210
- return createPage(client, parentPageId, title, blocks);
211
- }
212
26
  export async function getPage(client, pageId) {
213
27
  return (await client.pages.retrieve({ page_id: pageId }));
214
28
  }
package/dist/workflows.js CHANGED
@@ -5,7 +5,7 @@ import { resolve, dirname } from "path";
5
5
  import { validateEnv, writeEnvFile, PKG_ROOT } from "./env.js";
6
6
  import { loadConfig, getTemplate, resolvePriority, resolveSeverity, validateDocType, validateLabel, } from "./config.js";
7
7
  import { getLinearClient, validateLinearKey, createIssue, deleteIssue, getIssue, getIssueDetail, createRelation, createAttachment, createLabel, getTeams, getTeamStates, getTeamLabels, getTeamProjects, resolveLabels, } from "./linear.js";
8
- import { getNotionClient, createTemplatedPage, createDatabaseEntry, validateNotionKey, searchPages, createPageFromMarkdown, updatePageContent, deletePage, extractNotionPageId, } from "./notion.js";
8
+ import { getNotionClient, validateNotionKey, searchPages, createPageFromMarkdown, updatePageContent, deletePage, extractNotionPageId, } from "./notion.js";
9
9
  // ── Init ──
10
10
  function copyBundledFile(srcName, destPath) {
11
11
  if (existsSync(destPath)) {
@@ -243,24 +243,22 @@ async function startFeature(ctx, args) {
243
243
  const priority = tmpl.linear_priority
244
244
  ? resolvePriority(ctx.config, tmpl.linear_priority)
245
245
  : undefined;
246
+ const description = [
247
+ `## Tasks`,
248
+ `- [ ] Implementation`,
249
+ `- [ ] Write/update documentation (\`push-doc\`)`,
250
+ `- [ ] Tests`,
251
+ `- [ ] Review`,
252
+ ].join("\n");
246
253
  const issue = await createIssue(ctx.linear, {
247
254
  teamId: ctx.env.LINEAR_DEFAULT_TEAM_ID,
248
255
  title,
256
+ description,
249
257
  priority,
250
258
  labelIds,
251
259
  projectId: ctx.env.LINEAR_DEFAULT_PROJECT_ID,
252
260
  });
253
- console.log(`[Linear] Issue created: ${issue.identifier} — ${issue.url}`);
254
- if (!ctx.notion || !ctx.env.NOTION_ROOT_PAGE_ID) {
255
- console.log("[Notion] Not configured — skipping page creation");
256
- return;
257
- }
258
- const page = await createTemplatedPage(ctx.notion, ctx.env.NOTION_ROOT_PAGE_ID, tmpl.notion_template, title, issue.url);
259
- console.log(`[Notion] Page created: ${page.url}`);
260
- await createAttachment(ctx.linear, issue.id, page.url, `${title} — PRD`);
261
- console.log(`[Link] Linear ↔ Notion linked`);
262
- console.log(`\n✅ Feature started: ${issue.identifier} | Notion: ${page.url}`);
263
- console.log(` Notion Page ID: ${page.id} (use with --parent for sub-docs)`);
261
+ console.log(`✅ Feature started: ${issue.identifier} — ${issue.url}`);
264
262
  }
265
263
  async function reportBug(ctx, args) {
266
264
  const title = args._[0];
@@ -273,38 +271,25 @@ async function reportBug(ctx, args) {
273
271
  const teamLabels = await getTeamLabels(ctx.linear, ctx.env.LINEAR_DEFAULT_TEAM_ID);
274
272
  const configLabels = tmpl.linear_labels.map((lid) => validateLabel(ctx.config, lid).name);
275
273
  const labelIds = resolveLabels(configLabels, teamLabels);
274
+ const description = [
275
+ `**Severity: ${severity}**`,
276
+ ``,
277
+ `## Tasks`,
278
+ `- [ ] Reproduce`,
279
+ `- [ ] Root cause analysis`,
280
+ `- [ ] Fix & write tests`,
281
+ `- [ ] Write/update documentation (\`push-doc\`)`,
282
+ `- [ ] Review`,
283
+ ].join("\n");
276
284
  const issue = await createIssue(ctx.linear, {
277
285
  teamId: ctx.env.LINEAR_DEFAULT_TEAM_ID,
278
286
  title,
287
+ description,
279
288
  priority,
280
289
  labelIds,
281
290
  projectId: ctx.env.LINEAR_DEFAULT_PROJECT_ID,
282
291
  });
283
- console.log(`[Linear] Bug created: ${issue.identifier} (severity: ${severity}) — ${issue.url}`);
284
- if (!ctx.notion) {
285
- console.log("[Notion] Not configured — skipping");
286
- return;
287
- }
288
- let notionUrl;
289
- if (ctx.env.NOTION_BUG_DB_ID) {
290
- const entry = await createDatabaseEntry(ctx.notion, ctx.env.NOTION_BUG_DB_ID, {
291
- Name: { title: [{ text: { content: title } }] },
292
- });
293
- notionUrl = entry.url;
294
- console.log(`[Notion] Bug DB entry: ${notionUrl}`);
295
- }
296
- else if (ctx.env.NOTION_ROOT_PAGE_ID) {
297
- const page = await createTemplatedPage(ctx.notion, ctx.env.NOTION_ROOT_PAGE_ID, tmpl.notion_template, title, issue.url, severity);
298
- notionUrl = page.url;
299
- console.log(`[Notion] Bug report page: ${notionUrl}`);
300
- }
301
- else {
302
- console.log("[Notion] NOTION_ROOT_PAGE_ID not set — skipping");
303
- return;
304
- }
305
- await createAttachment(ctx.linear, issue.id, notionUrl, `${title} — Bug Report`);
306
- console.log(`[Link] Linear ↔ Notion linked`);
307
- console.log(`\n✅ Bug reported: ${issue.identifier} | Notion: ${notionUrl}`);
292
+ console.log(`✅ Bug reported: ${issue.identifier} (severity: ${severity}) — ${issue.url}`);
308
293
  }
309
294
  async function addTask(ctx, args) {
310
295
  const parentIdentifier = args._[0];
@@ -395,14 +380,14 @@ async function get(ctx, args) {
395
380
  }
396
381
  }
397
382
  async function pushDoc(ctx, args) {
398
- const identifier = args._[0];
399
- const filePath = args._[1];
383
+ const filePath = args._[0];
400
384
  const content = args.content;
401
385
  const title = args.title;
402
386
  const parentPageId = args.parent;
403
- if (!identifier || (!filePath && !content)) {
404
- throw new Error("Usage: npx pm-skill push-doc <issue> <file.md> [--title T] [--parent P]\n" +
405
- " npx pm-skill push-doc <issue> --title T --content \"# md\" [--parent P]");
387
+ const issueId = args.issue;
388
+ if (!filePath && !content) {
389
+ throw new Error("Usage: npx pm-skill push-doc <file.md> [--title T] [--parent P] [--issue I]\n" +
390
+ " npx pm-skill push-doc --title T --content \"# md\" [--parent P] [--issue I]");
406
391
  }
407
392
  if (!ctx.notion) {
408
393
  throw new Error("Notion is not configured. Run 'npx pm-skill init' with --notion-key.");
@@ -413,27 +398,31 @@ async function pushDoc(ctx, args) {
413
398
  }
414
399
  // Read markdown
415
400
  let markdown;
416
- if (filePath) {
417
- if (!existsSync(filePath)) {
418
- throw new Error(`File not found: ${filePath}`);
419
- }
401
+ if (filePath && !existsSync(filePath) && !content) {
402
+ throw new Error(`File not found: ${filePath}`);
403
+ }
404
+ if (filePath && existsSync(filePath)) {
420
405
  markdown = readFileSync(filePath, "utf-8");
421
406
  }
422
- else {
407
+ else if (content) {
423
408
  markdown = content;
424
409
  }
410
+ else {
411
+ throw new Error("Provide a file path or --content.");
412
+ }
425
413
  // Determine title
426
414
  const docTitle = title ?? (filePath ? filePath.replace(/^.*[\\/]/, "").replace(/\.md$/, "") : "Untitled");
427
- // Get Linear issue for linking
428
- const issue = await getIssue(ctx.linear, identifier);
429
- // Create Notion page under specified parent
415
+ // Create Notion page
430
416
  const page = await createPageFromMarkdown(ctx.notion, targetParent, docTitle, markdown);
431
417
  console.log(`[Notion] Page created: "${docTitle}" — ${page.url}`);
432
418
  console.log(`[Notion] Page ID: ${page.id}`);
433
- // Link to Linear issue
434
- await createAttachment(ctx.linear, issue.id, page.url, docTitle, "source-of-truth");
435
- console.log(`[Link] Attached to ${issue.identifier}`);
436
- console.log(`\n✅ Document pushed: ${issue.identifier} | ${page.url}`);
419
+ // Optionally link to Linear issue
420
+ if (issueId) {
421
+ const issue = await getIssue(ctx.linear, issueId);
422
+ await createAttachment(ctx.linear, issue.id, page.url, docTitle, "source-of-truth");
423
+ console.log(`[Link] Attached to ${issue.identifier}`);
424
+ }
425
+ console.log(`\n✅ Document pushed: ${page.url}`);
437
426
  }
438
427
  async function updateDoc(ctx, args) {
439
428
  const pageId = args._[0];
@@ -488,11 +477,43 @@ async function createFolder(ctx, args) {
488
477
  }
489
478
  async function del(ctx, args) {
490
479
  const identifiers = args._;
480
+ const recursive = !!args.recursive;
491
481
  if (identifiers.length === 0) {
492
- throw new Error("Usage: npx pm-skill delete <issue> [issue2 ...]");
482
+ throw new Error("Usage: npx pm-skill delete <issue> [issue2 ...] [--recursive]");
493
483
  }
494
484
  for (const identifier of identifiers) {
495
485
  const detail = await getIssueDetail(ctx.linear, identifier);
486
+ // Check for children
487
+ if (detail.children.length > 0 && !recursive) {
488
+ console.log(`⚠️ ${detail.issue.identifier} has ${detail.children.length} sub-issue(s):`);
489
+ for (const child of detail.children) {
490
+ console.log(` ${child.identifier}: ${child.title}`);
491
+ }
492
+ throw new Error(`Use --recursive to delete ${detail.issue.identifier} and its sub-issues.`);
493
+ }
494
+ // Recursively delete children first
495
+ if (detail.children.length > 0 && recursive) {
496
+ for (const child of detail.children) {
497
+ const childDetail = await getIssueDetail(ctx.linear, child.identifier);
498
+ // Delete child's Notion pages
499
+ if (ctx.notion) {
500
+ for (const att of childDetail.attachments) {
501
+ if (att.url.includes("notion.so")) {
502
+ const pageId = extractNotionPageId(att.url);
503
+ if (pageId) {
504
+ try {
505
+ await deletePage(ctx.notion, pageId);
506
+ console.log(` [Notion] Deleted: ${att.title}`);
507
+ }
508
+ catch { /* skip */ }
509
+ }
510
+ }
511
+ }
512
+ }
513
+ await deleteIssue(ctx.linear, child.id);
514
+ console.log(` Deleted sub-issue: ${child.identifier}`);
515
+ }
516
+ }
496
517
  // Delete linked Notion pages
497
518
  if (ctx.notion && detail.attachments.length > 0) {
498
519
  for (const att of detail.attachments) {
@@ -504,7 +525,7 @@ async function del(ctx, args) {
504
525
  console.log(` [Notion] Deleted: ${att.title}`);
505
526
  }
506
527
  catch {
507
- console.log(` [Notion] Could not delete: ${att.url} (may already be deleted or no access)`);
528
+ console.log(` [Notion] Could not delete: ${att.url}`);
508
529
  }
509
530
  }
510
531
  }
@@ -555,6 +576,51 @@ async function selectProject(args) {
555
576
  console.log(`\nUsage: npx pm-skill select-project "<name or id>"`);
556
577
  }
557
578
  }
579
+ async function selectPage(args) {
580
+ const { loadEnvFile, writeEnvFile } = await import("./env.js");
581
+ loadEnvFile();
582
+ const apiKey = process.env.NOTION_API_KEY;
583
+ if (!apiKey) {
584
+ throw new Error("NOTION_API_KEY must be set. Run 'npx pm-skill init' with --notion-key first.");
585
+ }
586
+ const client = getNotionClient(apiKey);
587
+ const pages = await searchPages(client, "");
588
+ if (pages.length === 0) {
589
+ console.log("No pages shared with this integration.");
590
+ console.log("Share a page in Notion: page menu → Connections → add your integration");
591
+ return;
592
+ }
593
+ const pageArg = args._[0];
594
+ if (pageArg) {
595
+ const match = pages.find((p) => p.id === pageArg || p.title.toLowerCase() === pageArg.toLowerCase());
596
+ if (!match) {
597
+ console.log("Available pages:");
598
+ for (const p of pages) {
599
+ console.log(` ${p.title} | ${p.id}`);
600
+ }
601
+ throw new Error(`Page '${pageArg}' not found.`);
602
+ }
603
+ writeEnvFile(process.cwd(), { NOTION_ROOT_PAGE_ID: match.id });
604
+ console.log(`✅ Selected page: "${match.title}" (${match.id})`);
605
+ }
606
+ else {
607
+ const currentId = process.env.NOTION_ROOT_PAGE_ID;
608
+ console.log("Available pages:");
609
+ for (const p of pages) {
610
+ const marker = p.id === currentId ? " ← current" : "";
611
+ console.log(` ${p.title} | ${p.id}${marker}`);
612
+ }
613
+ console.log(`\nUsage: npx pm-skill select-page "<name or id>"`);
614
+ }
615
+ }
616
+ async function installCodexSkill() {
617
+ const { homedir } = await import("os");
618
+ const targetDir = resolve(homedir(), ".codex", "skills", "pm-skill");
619
+ const targetPath = resolve(targetDir, "SKILL.md");
620
+ copyBundledFile("SKILL.md", targetPath);
621
+ console.log(`\n✅ Codex skill installed: ${targetPath}`);
622
+ console.log(` Restart Codex to pick up the new skill.`);
623
+ }
558
624
  // ── Command Registry ──
559
625
  const COMMANDS = {
560
626
  setup: (ctx, args) => setup(ctx, args),
@@ -573,8 +639,8 @@ const COMMANDS = {
573
639
  // ── Main ──
574
640
  async function main() {
575
641
  const args = minimist(process.argv.slice(2), {
576
- string: ["severity", "type", "url", "title", "content", "parent", "linear-key", "notion-key", "team-id", "project-id", "notion-page"],
577
- boolean: ["sync", "version"],
642
+ string: ["severity", "type", "url", "title", "content", "parent", "issue", "linear-key", "notion-key", "team-id", "project-id", "notion-page"],
643
+ boolean: ["sync", "version", "recursive"],
578
644
  alias: { s: "severity", t: "type" },
579
645
  });
580
646
  // --version
@@ -595,7 +661,9 @@ Commands:
595
661
  Initialize project (validates keys, creates .env, config.yml, SKILL.md, AGENTS.md)
596
662
  setup [--sync] Verify config & label matching (--sync creates missing labels)
597
663
  select-project [name-or-id] List or switch Linear project
598
- start-feature <title> Start feature (Linear issue + Notion PRD)
664
+ select-page [name-or-id] List or switch Notion root page
665
+ install-codex-skill Install skill to ~/.codex/skills/ for Codex
666
+ start-feature <title> Start feature (Linear issue with task checklist)
599
667
  report-bug <title> [--severity S] File bug report (severity: urgent/high/medium/low)
600
668
  add-task <parent> <title> Add sub-task to an issue
601
669
  relate <issue1> <issue2> [--type T] Link issues (type: related/similar)
@@ -603,15 +671,16 @@ Commands:
603
671
  attach-doc <issue> --url U --title T --type Y
604
672
  Attach document (type: source-of-truth/issue-tracking/domain-knowledge)
605
673
  get <issue> Show issue details
606
- push-doc <issue> <file.md> [--title T] [--parent P]
607
- Upload markdown to Notion + link to issue
608
- push-doc <issue> --title T --content "# md" [--parent P]
674
+ push-doc <file.md> [--title T] [--parent P] [--issue I]
675
+ Upload markdown to Notion (optionally link to issue)
676
+ push-doc --title T --content "# md" [--parent P] [--issue I]
609
677
  Push content directly (for AI agents)
610
678
  update-doc <page-id> <file.md> Replace Notion page content with markdown
611
679
  update-doc <page-id> --content "# md"
612
680
  Replace content directly
613
681
  create-folder <name> [--parent P] Create Notion folder (returns page ID for --parent)
614
- delete <issue> [issue2 ...] Delete issue(s) + linked Notion pages
682
+ delete <issue> [issue2 ...] [--recursive]
683
+ Delete issue(s) + linked Notion pages (--recursive for sub-issues)
615
684
  help Show this help
616
685
  --version Show version
617
686
 
@@ -627,6 +696,14 @@ All config is per-project (CWD). Run 'npx pm-skill init' in each project.`);
627
696
  await selectProject(args);
628
697
  return;
629
698
  }
699
+ if (command === "select-page") {
700
+ await selectPage(args);
701
+ return;
702
+ }
703
+ if (command === "install-codex-skill") {
704
+ await installCodexSkill();
705
+ return;
706
+ }
630
707
  const cmdFn = COMMANDS[command];
631
708
  if (!cmdFn) {
632
709
  const available = ["init", ...Object.keys(COMMANDS)].join(", ");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pm-skill",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Structured project management CLI — Linear + Notion integration for AI coding assistants (Claude Code, Codex)",
5
5
  "type": "module",
6
6
  "bin": {