pm-skill 1.1.3 → 1.1.4

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/dist/notion.d.ts CHANGED
@@ -32,6 +32,15 @@ export declare function searchPages(client: Client, query: string): Promise<Arra
32
32
  id: string;
33
33
  title: string;
34
34
  }>>;
35
+ /**
36
+ * Archive (delete) a Notion page by ID.
37
+ */
38
+ export declare function deletePage(client: Client, pageId: string): Promise<void>;
39
+ /**
40
+ * Extract Notion page ID from a Notion URL.
41
+ * Handles formats like: https://notion.so/abc123def456... or https://www.notion.so/workspace/Page-Title-abc123def456
42
+ */
43
+ export declare function extractNotionPageId(url: string): string | null;
35
44
  /**
36
45
  * Convert markdown to Notion blocks.
37
46
  */
package/dist/notion.js CHANGED
@@ -224,6 +224,26 @@ export async function searchPages(client, query) {
224
224
  "(untitled)",
225
225
  }));
226
226
  }
227
+ // ── Page Deletion ──
228
+ /**
229
+ * Archive (delete) a Notion page by ID.
230
+ */
231
+ export async function deletePage(client, pageId) {
232
+ await client.pages.update({ page_id: pageId, archived: true });
233
+ }
234
+ /**
235
+ * Extract Notion page ID from a Notion URL.
236
+ * Handles formats like: https://notion.so/abc123def456... or https://www.notion.so/workspace/Page-Title-abc123def456
237
+ */
238
+ export function extractNotionPageId(url) {
239
+ const match = url.match(/([a-f0-9]{32})(?:\?|$)/);
240
+ if (match) {
241
+ const raw = match[1];
242
+ // Format as UUID
243
+ return `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20)}`;
244
+ }
245
+ return null;
246
+ }
227
247
  // ── Markdown Upload ──
228
248
  /**
229
249
  * Convert markdown to Notion blocks.
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, } from "./notion.js";
8
+ import { getNotionClient, createTemplatedPage, createDatabaseEntry, validateNotionKey, searchPages, createPageFromMarkdown, updatePageContent, deletePage, extractNotionPageId, } from "./notion.js";
9
9
  // ── Init ──
10
10
  function copyBundledFile(srcName, destPath) {
11
11
  if (existsSync(destPath)) {
@@ -260,6 +260,7 @@ async function startFeature(ctx, args) {
260
260
  await createAttachment(ctx.linear, issue.id, page.url, `${title} — PRD`);
261
261
  console.log(`[Link] Linear ↔ Notion linked`);
262
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)`);
263
264
  }
264
265
  async function reportBug(ctx, args) {
265
266
  const title = args._[0];
@@ -398,13 +399,18 @@ async function pushDoc(ctx, args) {
398
399
  const filePath = args._[1];
399
400
  const content = args.content;
400
401
  const title = args.title;
402
+ const parentPageId = args.parent;
401
403
  if (!identifier || (!filePath && !content)) {
402
- throw new Error("Usage: npx pm-skill push-doc <issue> <file.md> [--title T]\n" +
403
- " npx pm-skill push-doc <issue> --title T --content \"# Markdown...\"");
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]");
404
406
  }
405
- if (!ctx.notion || !ctx.env.NOTION_ROOT_PAGE_ID) {
407
+ if (!ctx.notion) {
406
408
  throw new Error("Notion is not configured. Run 'npx pm-skill init' with --notion-key.");
407
409
  }
410
+ const targetParent = parentPageId ?? ctx.env.NOTION_ROOT_PAGE_ID;
411
+ if (!targetParent) {
412
+ throw new Error("No Notion parent page. Set NOTION_ROOT_PAGE_ID or use --parent <page-id>.");
413
+ }
408
414
  // Read markdown
409
415
  let markdown;
410
416
  if (filePath) {
@@ -420,9 +426,10 @@ async function pushDoc(ctx, args) {
420
426
  const docTitle = title ?? (filePath ? filePath.replace(/^.*[\\/]/, "").replace(/\.md$/, "") : "Untitled");
421
427
  // Get Linear issue for linking
422
428
  const issue = await getIssue(ctx.linear, identifier);
423
- // Create Notion page
424
- const page = await createPageFromMarkdown(ctx.notion, ctx.env.NOTION_ROOT_PAGE_ID, docTitle, markdown);
429
+ // Create Notion page under specified parent
430
+ const page = await createPageFromMarkdown(ctx.notion, targetParent, docTitle, markdown);
425
431
  console.log(`[Notion] Page created: "${docTitle}" — ${page.url}`);
432
+ console.log(`[Notion] Page ID: ${page.id}`);
426
433
  // Link to Linear issue
427
434
  await createAttachment(ctx.linear, issue.id, page.url, docTitle, "source-of-truth");
428
435
  console.log(`[Link] Attached to ${issue.identifier}`);
@@ -452,15 +459,100 @@ async function updateDoc(ctx, args) {
452
459
  await updatePageContent(ctx.notion, pageId, markdown);
453
460
  console.log(`✅ Page updated: ${pageId}`);
454
461
  }
462
+ async function createFolder(ctx, args) {
463
+ const folderName = args._[0];
464
+ const parentPageId = args.parent;
465
+ if (!folderName) {
466
+ throw new Error("Usage: npx pm-skill create-folder <name> [--parent <page-id>]");
467
+ }
468
+ if (!ctx.notion) {
469
+ throw new Error("Notion is not configured. Run 'npx pm-skill init' with --notion-key.");
470
+ }
471
+ const targetParent = parentPageId ?? ctx.env.NOTION_ROOT_PAGE_ID;
472
+ if (!targetParent) {
473
+ throw new Error("No Notion parent page. Set NOTION_ROOT_PAGE_ID or use --parent <page-id>.");
474
+ }
475
+ const response = await ctx.notion.pages.create({
476
+ parent: { page_id: targetParent },
477
+ properties: {
478
+ title: { title: [{ text: { content: folderName } }] },
479
+ },
480
+ children: [],
481
+ });
482
+ const pageId = response.id;
483
+ const url = `https://notion.so/${pageId.replace(/-/g, "")}`;
484
+ console.log(`✅ Folder created: "${folderName}"`);
485
+ console.log(` Page ID: ${pageId}`);
486
+ console.log(` URL: ${url}`);
487
+ console.log(`\nUse with: npx pm-skill push-doc <issue> <file> --parent ${pageId}`);
488
+ }
455
489
  async function del(ctx, args) {
456
490
  const identifiers = args._;
457
491
  if (identifiers.length === 0) {
458
492
  throw new Error("Usage: npx pm-skill delete <issue> [issue2 ...]");
459
493
  }
460
494
  for (const identifier of identifiers) {
461
- const issue = await getIssue(ctx.linear, identifier);
462
- await deleteIssue(ctx.linear, issue.id);
463
- console.log(`✅ Deleted: ${issue.identifier} (${issue.title})`);
495
+ const detail = await getIssueDetail(ctx.linear, identifier);
496
+ // Delete linked Notion pages
497
+ if (ctx.notion && detail.attachments.length > 0) {
498
+ for (const att of detail.attachments) {
499
+ if (att.url.includes("notion.so")) {
500
+ const pageId = extractNotionPageId(att.url);
501
+ if (pageId) {
502
+ try {
503
+ await deletePage(ctx.notion, pageId);
504
+ console.log(` [Notion] Deleted: ${att.title}`);
505
+ }
506
+ catch {
507
+ console.log(` [Notion] Could not delete: ${att.url} (may already be deleted or no access)`);
508
+ }
509
+ }
510
+ }
511
+ }
512
+ }
513
+ // Delete Linear issue
514
+ await deleteIssue(ctx.linear, detail.issue.id);
515
+ console.log(`✅ Deleted: ${detail.issue.identifier} (${detail.issue.title})`);
516
+ }
517
+ }
518
+ async function selectProject(args) {
519
+ // Load env manually — we need API key and team ID but not project ID
520
+ const { loadEnvFile, writeEnvFile } = await import("./env.js");
521
+ loadEnvFile();
522
+ const apiKey = process.env.LINEAR_API_KEY;
523
+ const teamId = process.env.LINEAR_DEFAULT_TEAM_ID;
524
+ if (!apiKey || !teamId) {
525
+ throw new Error("LINEAR_API_KEY and LINEAR_DEFAULT_TEAM_ID must be set. Run 'npx pm-skill init' first.");
526
+ }
527
+ const client = getLinearClient(apiKey);
528
+ const projects = await getTeamProjects(client, teamId);
529
+ if (projects.length === 0) {
530
+ console.log("No projects found for this team.");
531
+ return;
532
+ }
533
+ const projectIdArg = args._[0];
534
+ if (projectIdArg) {
535
+ // Direct selection by ID or name
536
+ const match = projects.find((p) => p.id === projectIdArg || p.name.toLowerCase() === projectIdArg.toLowerCase());
537
+ if (!match) {
538
+ console.log("Available projects:");
539
+ for (const p of projects) {
540
+ console.log(` ${p.name} | ${p.id}`);
541
+ }
542
+ throw new Error(`Project '${projectIdArg}' not found.`);
543
+ }
544
+ writeEnvFile(process.cwd(), { LINEAR_DEFAULT_PROJECT_ID: match.id });
545
+ console.log(`✅ Selected project: "${match.name}" (${match.id})`);
546
+ }
547
+ else {
548
+ // List projects with current marker
549
+ const currentId = process.env.LINEAR_DEFAULT_PROJECT_ID;
550
+ console.log("Available projects:");
551
+ for (const p of projects) {
552
+ const marker = p.id === currentId ? " ← current" : "";
553
+ console.log(` ${p.name} | ${p.id}${marker}`);
554
+ }
555
+ console.log(`\nUsage: npx pm-skill select-project "<name or id>"`);
464
556
  }
465
557
  }
466
558
  // ── Command Registry ──
@@ -474,13 +566,14 @@ const COMMANDS = {
474
566
  "attach-doc": attachDoc,
475
567
  "push-doc": pushDoc,
476
568
  "update-doc": updateDoc,
569
+ "create-folder": createFolder,
477
570
  delete: del,
478
571
  get,
479
572
  };
480
573
  // ── Main ──
481
574
  async function main() {
482
575
  const args = minimist(process.argv.slice(2), {
483
- string: ["severity", "type", "url", "title", "content", "linear-key", "notion-key", "team-id", "project-id", "notion-page"],
576
+ string: ["severity", "type", "url", "title", "content", "parent", "linear-key", "notion-key", "team-id", "project-id", "notion-page"],
484
577
  boolean: ["sync", "version"],
485
578
  alias: { s: "severity", t: "type" },
486
579
  });
@@ -501,6 +594,7 @@ Commands:
501
594
  init --linear-key K [--notion-key K]
502
595
  Initialize project (validates keys, creates .env, config.yml, SKILL.md, AGENTS.md)
503
596
  setup [--sync] Verify config & label matching (--sync creates missing labels)
597
+ select-project [name-or-id] List or switch Linear project
504
598
  start-feature <title> Start feature (Linear issue + Notion PRD)
505
599
  report-bug <title> [--severity S] File bug report (severity: urgent/high/medium/low)
506
600
  add-task <parent> <title> Add sub-task to an issue
@@ -509,25 +603,30 @@ Commands:
509
603
  attach-doc <issue> --url U --title T --type Y
510
604
  Attach document (type: source-of-truth/issue-tracking/domain-knowledge)
511
605
  get <issue> Show issue details
512
- push-doc <issue> <file.md> [--title T]
606
+ push-doc <issue> <file.md> [--title T] [--parent P]
513
607
  Upload markdown to Notion + link to issue
514
- push-doc <issue> --title T --content "# md"
608
+ push-doc <issue> --title T --content "# md" [--parent P]
515
609
  Push content directly (for AI agents)
516
610
  update-doc <page-id> <file.md> Replace Notion page content with markdown
517
611
  update-doc <page-id> --content "# md"
518
612
  Replace content directly
519
- delete <issue> [issue2 ...] Delete issue(s)
613
+ create-folder <name> [--parent P] Create Notion folder (returns page ID for --parent)
614
+ delete <issue> [issue2 ...] Delete issue(s) + linked Notion pages
520
615
  help Show this help
521
616
  --version Show version
522
617
 
523
618
  All config is per-project (CWD). Run 'npx pm-skill init' in each project.`);
524
619
  return;
525
620
  }
526
- // init runs independently — no env/config validation
621
+ // These commands run independently — no full env/config validation
527
622
  if (command === "init") {
528
623
  await init(args);
529
624
  return;
530
625
  }
626
+ if (command === "select-project") {
627
+ await selectProject(args);
628
+ return;
629
+ }
531
630
  const cmdFn = COMMANDS[command];
532
631
  if (!cmdFn) {
533
632
  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.3",
3
+ "version": "1.1.4",
4
4
  "description": "Structured project management CLI — Linear + Notion integration for AI coding assistants (Claude Code, Codex)",
5
5
  "type": "module",
6
6
  "bin": {