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 +9 -0
- package/dist/notion.js +20 -0
- package/dist/workflows.js +113 -14
- package/package.json +1 -1
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 \"#
|
|
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
|
|
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,
|
|
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
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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(", ");
|