pm-skill 1.1.1 → 1.1.3
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/linear.d.ts +5 -0
- package/dist/linear.js +8 -0
- package/dist/notion.d.ts +16 -0
- package/dist/notion.js +55 -0
- package/dist/workflows.js +131 -8
- package/package.json +5 -4
package/dist/linear.d.ts
CHANGED
|
@@ -34,6 +34,11 @@ export interface IssueDetail {
|
|
|
34
34
|
}
|
|
35
35
|
export declare function createIssue(client: LinearClient, opts: CreateIssueOpts): Promise<Issue>;
|
|
36
36
|
export declare function updateIssue(client: LinearClient, id: string, input: Record<string, unknown>): Promise<void>;
|
|
37
|
+
export declare function deleteIssue(client: LinearClient, id: string): Promise<void>;
|
|
38
|
+
export declare function getTeamProjects(client: LinearClient, teamId: string): Promise<Array<{
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
}>>;
|
|
37
42
|
export declare function getIssue(client: LinearClient, identifier: string): Promise<Issue>;
|
|
38
43
|
export declare function getIssueDetail(client: LinearClient, identifier: string): Promise<IssueDetail>;
|
|
39
44
|
export declare function createRelation(client: LinearClient, issueId: string, relatedIssueId: string, type: "blocks" | "related" | "similar"): Promise<void>;
|
package/dist/linear.js
CHANGED
|
@@ -42,6 +42,14 @@ export async function createIssue(client, opts) {
|
|
|
42
42
|
export async function updateIssue(client, id, input) {
|
|
43
43
|
await client.updateIssue(id, input);
|
|
44
44
|
}
|
|
45
|
+
export async function deleteIssue(client, id) {
|
|
46
|
+
await client.deleteIssue(id);
|
|
47
|
+
}
|
|
48
|
+
export async function getTeamProjects(client, teamId) {
|
|
49
|
+
const team = await client.team(teamId);
|
|
50
|
+
const conn = await team.projects();
|
|
51
|
+
return conn.nodes.map((p) => ({ id: p.id, name: p.name }));
|
|
52
|
+
}
|
|
45
53
|
export async function getIssue(client, identifier) {
|
|
46
54
|
try {
|
|
47
55
|
return await client.issue(identifier);
|
package/dist/notion.d.ts
CHANGED
|
@@ -32,3 +32,19 @@ export declare function searchPages(client: Client, query: string): Promise<Arra
|
|
|
32
32
|
id: string;
|
|
33
33
|
title: string;
|
|
34
34
|
}>>;
|
|
35
|
+
/**
|
|
36
|
+
* Convert markdown to Notion blocks.
|
|
37
|
+
*/
|
|
38
|
+
export declare function mdToBlocks(markdown: string): BlockObjectRequest[];
|
|
39
|
+
/**
|
|
40
|
+
* Create a Notion page from markdown content.
|
|
41
|
+
* Handles the 100-block-per-request API limit by chunking.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createPageFromMarkdown(client: Client, parentPageId: string, title: string, markdown: string): Promise<{
|
|
44
|
+
id: string;
|
|
45
|
+
url: string;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Clear all blocks from a page, then append new markdown content.
|
|
49
|
+
*/
|
|
50
|
+
export declare function updatePageContent(client: Client, pageId: string, markdown: string): Promise<void>;
|
package/dist/notion.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Client } from "@notionhq/client";
|
|
2
|
+
import { markdownToBlocks } from "@tryfabric/martian";
|
|
2
3
|
// ── Client ──
|
|
3
4
|
let _client = null;
|
|
4
5
|
export function getNotionClient(apiKey) {
|
|
@@ -223,3 +224,57 @@ export async function searchPages(client, query) {
|
|
|
223
224
|
"(untitled)",
|
|
224
225
|
}));
|
|
225
226
|
}
|
|
227
|
+
// ── Markdown Upload ──
|
|
228
|
+
/**
|
|
229
|
+
* Convert markdown to Notion blocks.
|
|
230
|
+
*/
|
|
231
|
+
export function mdToBlocks(markdown) {
|
|
232
|
+
return markdownToBlocks(markdown);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Create a Notion page from markdown content.
|
|
236
|
+
* Handles the 100-block-per-request API limit by chunking.
|
|
237
|
+
*/
|
|
238
|
+
export async function createPageFromMarkdown(client, parentPageId, title, markdown) {
|
|
239
|
+
const blocks = mdToBlocks(markdown);
|
|
240
|
+
// First batch goes with page creation (max 100)
|
|
241
|
+
const firstBatch = blocks.slice(0, 100);
|
|
242
|
+
const rest = blocks.slice(100);
|
|
243
|
+
const response = await client.pages.create({
|
|
244
|
+
parent: { page_id: parentPageId },
|
|
245
|
+
properties: {
|
|
246
|
+
title: { title: [{ text: { content: title } }] },
|
|
247
|
+
},
|
|
248
|
+
children: firstBatch,
|
|
249
|
+
});
|
|
250
|
+
const pageId = response.id;
|
|
251
|
+
// Append remaining blocks in chunks of 100
|
|
252
|
+
for (let i = 0; i < rest.length; i += 100) {
|
|
253
|
+
await client.blocks.children.append({
|
|
254
|
+
block_id: pageId,
|
|
255
|
+
children: rest.slice(i, i + 100),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
id: pageId,
|
|
260
|
+
url: `https://notion.so/${pageId.replace(/-/g, "")}`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Clear all blocks from a page, then append new markdown content.
|
|
265
|
+
*/
|
|
266
|
+
export async function updatePageContent(client, pageId, markdown) {
|
|
267
|
+
// 1. Delete existing blocks
|
|
268
|
+
const existing = await client.blocks.children.list({ block_id: pageId });
|
|
269
|
+
for (const block of existing.results) {
|
|
270
|
+
await client.blocks.delete({ block_id: block.id });
|
|
271
|
+
}
|
|
272
|
+
// 2. Append new blocks
|
|
273
|
+
const blocks = mdToBlocks(markdown);
|
|
274
|
+
for (let i = 0; i < blocks.length; i += 100) {
|
|
275
|
+
await client.blocks.children.append({
|
|
276
|
+
block_id: pageId,
|
|
277
|
+
children: blocks.slice(i, i + 100),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
package/dist/workflows.js
CHANGED
|
@@ -4,8 +4,8 @@ import { existsSync, copyFileSync, mkdirSync, readFileSync } from "fs";
|
|
|
4
4
|
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
|
-
import { getLinearClient, validateLinearKey, createIssue, getIssue, getIssueDetail, createRelation, createAttachment, createLabel, getTeams, getTeamStates, getTeamLabels, resolveLabels, } from "./linear.js";
|
|
8
|
-
import { getNotionClient, createTemplatedPage, createDatabaseEntry, validateNotionKey, } from "./notion.js";
|
|
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";
|
|
9
9
|
// ── Init ──
|
|
10
10
|
function copyBundledFile(srcName, destPath) {
|
|
11
11
|
if (existsSync(destPath)) {
|
|
@@ -69,11 +69,53 @@ async function init(args) {
|
|
|
69
69
|
throw new Error(`Team '${teamId}' not found. Run 'npx pm-skill init --linear-key <key>' to see available teams.`);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
// 3.
|
|
72
|
+
// 3. Auto-detect project
|
|
73
|
+
let selectedProjectId = projectId;
|
|
74
|
+
if (!selectedProjectId) {
|
|
75
|
+
const projects = await getTeamProjects(client, selectedTeamId);
|
|
76
|
+
if (projects.length === 0) {
|
|
77
|
+
console.log(" No projects found — skipping project assignment");
|
|
78
|
+
}
|
|
79
|
+
else if (projects.length === 1) {
|
|
80
|
+
selectedProjectId = projects[0].id;
|
|
81
|
+
console.log(` Auto-selected project: "${projects[0].name}"`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log(`\n Available projects (${projects.length}):`);
|
|
85
|
+
for (const proj of projects) {
|
|
86
|
+
console.log(` ${proj.name} | ${proj.id}`);
|
|
87
|
+
}
|
|
88
|
+
selectedProjectId = projects[0].id;
|
|
89
|
+
console.log(` Using first project: "${projects[0].name}". Override with --project-id <id>`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 4. Validate Notion key + auto-detect root page
|
|
93
|
+
let selectedNotionPage = notionPage;
|
|
73
94
|
if (notionKey) {
|
|
74
95
|
console.log("\n[Notion] Validating API key...");
|
|
75
96
|
const notionUser = await validateNotionKey(notionKey);
|
|
76
97
|
console.log(` Authenticated as: ${notionUser.name}`);
|
|
98
|
+
if (!selectedNotionPage) {
|
|
99
|
+
console.log(" Searching for accessible pages...");
|
|
100
|
+
const notionClient = getNotionClient(notionKey);
|
|
101
|
+
const pages = await searchPages(notionClient, "");
|
|
102
|
+
if (pages.length === 0) {
|
|
103
|
+
console.log(" ⚠️ No pages shared with this integration.");
|
|
104
|
+
console.log(" Share a page in Notion: page menu → Connections → add your integration");
|
|
105
|
+
}
|
|
106
|
+
else if (pages.length === 1) {
|
|
107
|
+
selectedNotionPage = pages[0].id;
|
|
108
|
+
console.log(` Auto-selected root page: "${pages[0].title}" (${pages[0].id})`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log(`\n Accessible pages (${pages.length}):`);
|
|
112
|
+
for (const page of pages) {
|
|
113
|
+
console.log(` ${page.title} | ${page.id}`);
|
|
114
|
+
}
|
|
115
|
+
selectedNotionPage = pages[0].id;
|
|
116
|
+
console.log(` Using first page: "${pages[0].title}". Override with --notion-page <id>`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
77
119
|
}
|
|
78
120
|
// 4. Write .env
|
|
79
121
|
console.log("\n[Config] Writing .env...");
|
|
@@ -81,12 +123,12 @@ async function init(args) {
|
|
|
81
123
|
LINEAR_API_KEY: linearKey,
|
|
82
124
|
LINEAR_DEFAULT_TEAM_ID: selectedTeamId,
|
|
83
125
|
};
|
|
84
|
-
if (
|
|
85
|
-
envEntries.LINEAR_DEFAULT_PROJECT_ID =
|
|
126
|
+
if (selectedProjectId)
|
|
127
|
+
envEntries.LINEAR_DEFAULT_PROJECT_ID = selectedProjectId;
|
|
86
128
|
if (notionKey)
|
|
87
129
|
envEntries.NOTION_API_KEY = notionKey;
|
|
88
|
-
if (
|
|
89
|
-
envEntries.NOTION_ROOT_PAGE_ID =
|
|
130
|
+
if (selectedNotionPage)
|
|
131
|
+
envEntries.NOTION_ROOT_PAGE_ID = selectedNotionPage;
|
|
90
132
|
const envPath = writeEnvFile(cwd, envEntries);
|
|
91
133
|
console.log(` Written: ${envPath}`);
|
|
92
134
|
// 5. Copy config.yml, SKILL.md, AGENTS.md
|
|
@@ -351,6 +393,76 @@ async function get(ctx, args) {
|
|
|
351
393
|
}
|
|
352
394
|
}
|
|
353
395
|
}
|
|
396
|
+
async function pushDoc(ctx, args) {
|
|
397
|
+
const identifier = args._[0];
|
|
398
|
+
const filePath = args._[1];
|
|
399
|
+
const content = args.content;
|
|
400
|
+
const title = args.title;
|
|
401
|
+
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
|
+
}
|
|
405
|
+
if (!ctx.notion || !ctx.env.NOTION_ROOT_PAGE_ID) {
|
|
406
|
+
throw new Error("Notion is not configured. Run 'npx pm-skill init' with --notion-key.");
|
|
407
|
+
}
|
|
408
|
+
// Read markdown
|
|
409
|
+
let markdown;
|
|
410
|
+
if (filePath) {
|
|
411
|
+
if (!existsSync(filePath)) {
|
|
412
|
+
throw new Error(`File not found: ${filePath}`);
|
|
413
|
+
}
|
|
414
|
+
markdown = readFileSync(filePath, "utf-8");
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
markdown = content;
|
|
418
|
+
}
|
|
419
|
+
// Determine title
|
|
420
|
+
const docTitle = title ?? (filePath ? filePath.replace(/^.*[\\/]/, "").replace(/\.md$/, "") : "Untitled");
|
|
421
|
+
// Get Linear issue for linking
|
|
422
|
+
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);
|
|
425
|
+
console.log(`[Notion] Page created: "${docTitle}" — ${page.url}`);
|
|
426
|
+
// Link to Linear issue
|
|
427
|
+
await createAttachment(ctx.linear, issue.id, page.url, docTitle, "source-of-truth");
|
|
428
|
+
console.log(`[Link] Attached to ${issue.identifier}`);
|
|
429
|
+
console.log(`\n✅ Document pushed: ${issue.identifier} | ${page.url}`);
|
|
430
|
+
}
|
|
431
|
+
async function updateDoc(ctx, args) {
|
|
432
|
+
const pageId = args._[0];
|
|
433
|
+
const filePath = args._[1];
|
|
434
|
+
const content = args.content;
|
|
435
|
+
if (!pageId || (!filePath && !content)) {
|
|
436
|
+
throw new Error("Usage: npx pm-skill update-doc <page-id> <file.md>\n" +
|
|
437
|
+
" npx pm-skill update-doc <page-id> --content \"# Updated...\"");
|
|
438
|
+
}
|
|
439
|
+
if (!ctx.notion) {
|
|
440
|
+
throw new Error("Notion is not configured. Run 'npx pm-skill init' with --notion-key.");
|
|
441
|
+
}
|
|
442
|
+
let markdown;
|
|
443
|
+
if (filePath) {
|
|
444
|
+
if (!existsSync(filePath)) {
|
|
445
|
+
throw new Error(`File not found: ${filePath}`);
|
|
446
|
+
}
|
|
447
|
+
markdown = readFileSync(filePath, "utf-8");
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
markdown = content;
|
|
451
|
+
}
|
|
452
|
+
await updatePageContent(ctx.notion, pageId, markdown);
|
|
453
|
+
console.log(`✅ Page updated: ${pageId}`);
|
|
454
|
+
}
|
|
455
|
+
async function del(ctx, args) {
|
|
456
|
+
const identifiers = args._;
|
|
457
|
+
if (identifiers.length === 0) {
|
|
458
|
+
throw new Error("Usage: npx pm-skill delete <issue> [issue2 ...]");
|
|
459
|
+
}
|
|
460
|
+
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})`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
354
466
|
// ── Command Registry ──
|
|
355
467
|
const COMMANDS = {
|
|
356
468
|
setup: (ctx, args) => setup(ctx, args),
|
|
@@ -360,12 +472,15 @@ const COMMANDS = {
|
|
|
360
472
|
relate,
|
|
361
473
|
block,
|
|
362
474
|
"attach-doc": attachDoc,
|
|
475
|
+
"push-doc": pushDoc,
|
|
476
|
+
"update-doc": updateDoc,
|
|
477
|
+
delete: del,
|
|
363
478
|
get,
|
|
364
479
|
};
|
|
365
480
|
// ── Main ──
|
|
366
481
|
async function main() {
|
|
367
482
|
const args = minimist(process.argv.slice(2), {
|
|
368
|
-
string: ["severity", "type", "url", "title", "linear-key", "notion-key", "team-id", "project-id", "notion-page"],
|
|
483
|
+
string: ["severity", "type", "url", "title", "content", "linear-key", "notion-key", "team-id", "project-id", "notion-page"],
|
|
369
484
|
boolean: ["sync", "version"],
|
|
370
485
|
alias: { s: "severity", t: "type" },
|
|
371
486
|
});
|
|
@@ -394,6 +509,14 @@ Commands:
|
|
|
394
509
|
attach-doc <issue> --url U --title T --type Y
|
|
395
510
|
Attach document (type: source-of-truth/issue-tracking/domain-knowledge)
|
|
396
511
|
get <issue> Show issue details
|
|
512
|
+
push-doc <issue> <file.md> [--title T]
|
|
513
|
+
Upload markdown to Notion + link to issue
|
|
514
|
+
push-doc <issue> --title T --content "# md"
|
|
515
|
+
Push content directly (for AI agents)
|
|
516
|
+
update-doc <page-id> <file.md> Replace Notion page content with markdown
|
|
517
|
+
update-doc <page-id> --content "# md"
|
|
518
|
+
Replace content directly
|
|
519
|
+
delete <issue> [issue2 ...] Delete issue(s)
|
|
397
520
|
help Show this help
|
|
398
521
|
--version Show version
|
|
399
522
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pm-skill",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Structured project management CLI — Linear + Notion integration for AI coding assistants (Claude Code, Codex)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,14 +48,15 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@linear/sdk": "^80",
|
|
50
50
|
"@notionhq/client": "^5",
|
|
51
|
+
"@tryfabric/martian": "^1.2.4",
|
|
51
52
|
"js-yaml": "^4",
|
|
52
53
|
"minimist": "^1"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
|
-
"typescript": "^5",
|
|
56
|
-
"tsx": "^4",
|
|
57
56
|
"@types/js-yaml": "^4",
|
|
58
57
|
"@types/minimist": "^1",
|
|
59
|
-
"@types/node": "^22"
|
|
58
|
+
"@types/node": "^22",
|
|
59
|
+
"tsx": "^4",
|
|
60
|
+
"typescript": "^5"
|
|
60
61
|
}
|
|
61
62
|
}
|