@fink-andreas/pi-linear-tools 0.7.0 → 0.7.1

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/CHANGELOG.md CHANGED
@@ -1,25 +1,35 @@
1
1
  # Changelog
2
2
 
3
- ## v0.7.0 (2026-05-05)
3
+ ## v0.7.1 (2026-06-09)
4
+
5
+ Patch release for issue creation milestone assignment and configuration text cleanup.
6
+
7
+ ### Bug Fixes
8
+ - **Fixed issue create milestone assignment**: `linear_issue create` now assigns project milestones when `milestone` or `projectMilestoneId` is provided.
9
+ - **Fixed configuration text typo**: Corrected config command/help text in the pi extension.
10
+
11
+ ### Release Maintenance
12
+ - Updated package metadata to `0.7.1`.
13
+
14
+ ## v0.7.0 (2026-06-09)
4
15
 
5
16
  Issue image fetching and file download release.
6
17
 
7
18
  ### New Features
8
- - **`linear_issue(action="images")`**: Fetch markdown/HTML embedded images from issue descriptions and comments. Returns images as tool `image` content (base64-encoded). Includes special auth handling for Linear upload URLs (`uploads.linear.app`).
9
- - **`linear_issue(action="download")`**: Download Linear issue attachments to local filesystem. Supports attachment selection by ID, title, URL, or index. Includes relative path validation, filename sanitization, and `allow_overwrite_files` config guard (default: `false`).
19
+ - **Issue image fetching**: Added `linear_issue(action="images")` to fetch embedded markdown/HTML images from issue descriptions and comments as tool image content.
20
+ - **Issue attachment downloads**: Added `linear_issue(action="download")` to download Linear issue attachments to validated local paths with filename sanitization and overwrite controls.
21
+ - **Workspace-wide issue listing**: `linear_issue list` can now list across the workspace without requiring a project context.
22
+ - **Overwrite configuration**: Added `allow_overwrite_files` config support via `/linear-tools-config` and `pi-linear-tools config`.
10
23
 
11
- ### Performance / API Usage
12
- - **N+1 issue resolution optimization**: Merged N+1-per-issue lazy loads in milestone details into a single GraphQL batch query (introduced in v0.6.0).
24
+ ### Bug Fixes
25
+ - Fixed project archive mutation so `archiveProject` archives instead of deleting projects.
26
+ - Fixed comment rendering so submitted comment text appears in `linear_issue comment` results.
27
+ - Fixed rate-limit display to reflect actual complexity-based limits instead of a hardcoded total.
28
+ - Added project and project-update rate-limit pre-checks.
13
29
 
14
30
  ### Documentation
15
- - **Updated smoke tests**: Added coverage for `images` and `download` actions.
16
-
17
- ### Bug Fixes
18
- - **Fixed workspace-wide issue listing (#4)**: `linear_issue list` now falls back to workspace-level listing (using `fetchIssues`) when no project is specified and cwd doesn't match a Linear project. This allows `linear_issue list assignee=me` to work without requiring a project context.
19
- - Fixed project archive mutation to actually archive instead of delete (INN-301).
20
- - Fixed `linear_issue comment` to render submitted comment text in result (INN-305).
21
- - Fixed rate limit display to not show hardcoded total=5000 (INN-304).
22
- - Fixed `linear_project` and `linear_project_update` to have rate-limit pre-check (INN-303).
31
+ - Added smoke test coverage for image and attachment workflows.
32
+ - Added dedicated release notes in `docs/RELEASE_NOTES_v0.7.0.md`.
23
33
 
24
34
  ## v0.6.0 (2026-04-23)
25
35
 
@@ -358,7 +358,7 @@ async function runInteractiveConfigFlow(ctx, pi) {
358
358
  }
359
359
 
360
360
  if (!client) {
361
- const selectedAuthMethod = await ctx.ui.select('Select authentication method', ['API Key (recommended for full functionlaity)', 'OAuth']);
361
+ const selectedAuthMethod = await ctx.ui.select('Select authentication method', ['API Key (recommended for full functionality)', 'OAuth']);
362
362
 
363
363
  if (!selectedAuthMethod) {
364
364
  ctx.ui.notify('Configuration cancelled', 'warning');
@@ -748,11 +748,11 @@ async function registerLinearTools(pi) {
748
748
  },
749
749
  milestone: {
750
750
  type: 'string',
751
- description: 'For update: milestone name/ID, or "none" to clear milestone assignment.',
751
+ description: 'For create/update: milestone name/ID. For create, also provide project so names can be resolved. Use "none" to clear on update.',
752
752
  },
753
753
  projectMilestoneId: {
754
754
  type: 'string',
755
- description: 'Optional explicit milestone ID alias for update.',
755
+ description: 'Optional explicit milestone ID alias for create/update.',
756
756
  },
757
757
  subIssueOf: {
758
758
  type: 'string',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fink-andreas/pi-linear-tools",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Pi extension with Linear SDK tools and configuration commands",
5
5
  "type": "module",
6
6
  "engines": {
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "scripts": {
29
29
  "start": "node index.js",
30
- "test": "node tests/test-package-manifest.js && node tests/test-extension-registration.js && node tests/test-issue-comment-result.js && node tests/test-issue-priority.js && node tests/test-settings.js && node tests/test-issue-download.js && node tests/test-api-usage-caching.js && node tests/test-assignee-update.js && node tests/test-rate-limit-fallback-update.js && node tests/test-full-assignee-flow.js && node tests/test-branch-param.js && node tests/test-team-filter.js && node tests/test-project-crud.js && node tests/test-project-lifecycle.js && node tests/test-sync-doc.js && node tests/test-issue-activity.js",
30
+ "test": "node tests/test-package-manifest.js && node tests/test-extension-registration.js && node tests/test-issue-comment-result.js && node tests/test-issue-priority.js && node tests/test-settings.js && node tests/test-issue-download.js && node tests/test-api-usage-caching.js && node tests/test-issue-create-milestone.js && node tests/test-assignee-update.js && node tests/test-rate-limit-fallback-update.js && node tests/test-full-assignee-flow.js && node tests/test-branch-param.js && node tests/test-team-filter.js && node tests/test-project-crud.js && node tests/test-project-lifecycle.js && node tests/test-sync-doc.js && node tests/test-issue-activity.js",
31
31
  "dev:sync-local-extension": "node scripts/dev-sync-local-extension.mjs",
32
32
  "release:check": "npm test && npm pack --dry-run"
33
33
  },
package/src/handlers.js CHANGED
@@ -25,7 +25,6 @@ import {
25
25
  fetchIssueActivity,
26
26
  formatIssueAsMarkdown,
27
27
  formatIssueActivityAsMarkdown,
28
- fetchIssues,
29
28
  fetchIssuesByProject,
30
29
  fetchProjectMilestones,
31
30
  fetchMilestoneDetails,
@@ -390,24 +389,12 @@ async function startGitBranch(branchName, fromRef = 'HEAD', onBranchExists = 'sw
390
389
  export async function executeIssueList(client, params) {
391
390
  return withHandlerErrorHandling(async () => {
392
391
  let projectRef = params.project;
393
- let projectMode = false;
394
-
395
- if (projectRef) {
396
- // Explicit project specified - use project-scoped listing
397
- projectMode = true;
398
- } else {
399
- // No explicit project - try cwd fallback
400
- const cwdProject = path.basename(process.cwd());
401
- try {
402
- const resolved = await resolveProjectRef(client, cwdProject);
403
- projectRef = resolved.name; // use resolved name for display
404
- projectMode = true;
405
- } catch {
406
- // cwd doesn't match any project - fall back to workspace-level listing
407
- projectMode = false;
408
- }
392
+ if (!projectRef) {
393
+ projectRef = path.basename(process.cwd());
409
394
  }
410
395
 
396
+ const resolved = await resolveProjectRef(client, projectRef);
397
+
411
398
  let assigneeId = null;
412
399
  if (params.assignee === 'me') {
413
400
  const viewer = await getViewer(client);
@@ -421,50 +408,21 @@ export async function executeIssueList(client, params) {
421
408
  teamId = team.id;
422
409
  }
423
410
 
424
- const limit = params.limit || 20;
425
-
426
- let issues;
427
- let truncated;
428
- let projectId = null;
429
- let projectName = null;
430
-
431
- if (projectMode) {
432
- // Project-scoped listing
433
- const resolved = await resolveProjectRef(client, projectRef);
434
- projectId = resolved.id;
435
- projectName = resolved.name;
436
-
437
- const result = await fetchIssuesByProject(client, resolved.id, params.states || null, {
438
- assigneeId,
439
- teamId,
440
- limit,
441
- });
442
- issues = result.issues;
443
- truncated = result.truncated;
444
- } else {
445
- // Workspace-level listing (no project filter)
446
- // Default to open states if none specified
447
- const workspaceStates = params.states || ['Backlog', 'Triage', 'In Progress', 'In Review'];
448
- const result = await fetchIssues(client, assigneeId, workspaceStates, limit);
449
- issues = result.issues;
450
- truncated = result.truncated;
451
- }
411
+ const { issues, truncated } = await fetchIssuesByProject(client, resolved.id, params.states || null, {
412
+ assigneeId,
413
+ teamId,
414
+ limit: params.limit || 20,
415
+ });
452
416
 
453
417
  if (issues.length === 0) {
454
- return toTextResult(
455
- projectMode
456
- ? `No issues found in project "${projectName}"`
457
- : 'No issues found in workspace',
458
- {
459
- projectId,
460
- projectName,
461
- issueCount: 0,
462
- }
463
- );
418
+ return toTextResult(`No issues found in project "${resolved.name}"`, {
419
+ projectId: resolved.id,
420
+ projectName: resolved.name,
421
+ issueCount: 0,
422
+ });
464
423
  }
465
424
 
466
- const scopeLabel = projectMode ? `project "${projectName}"` : 'workspace';
467
- const lines = [`## Issues in ${scopeLabel} (${issues.length}${truncated ? '+' : ''})\n`];
425
+ const lines = [`## Issues in project "${resolved.name}" (${issues.length}${truncated ? '+' : ''})\n`];
468
426
 
469
427
  for (const issue of issues) {
470
428
  const stateLabel = issue.state?.name || 'Unknown';
@@ -484,8 +442,8 @@ export async function executeIssueList(client, params) {
484
442
  }
485
443
 
486
444
  return toTextResult(lines.join('\n'), {
487
- projectId,
488
- projectName,
445
+ projectId: resolved.id,
446
+ projectName: resolved.name,
489
447
  issueCount: issues.length,
490
448
  truncated,
491
449
  });
@@ -732,6 +690,22 @@ export async function executeIssueCreate(client, params, options = {}) {
732
690
  createInput.projectId = resolvedProject.id;
733
691
  }
734
692
 
693
+ if (params.projectMilestoneId !== undefined && params.projectMilestoneId !== null) {
694
+ createInput.projectMilestoneId = params.projectMilestoneId;
695
+ } else if (params.milestone !== undefined && params.milestone !== null) {
696
+ const milestoneRef = String(params.milestone || '').trim();
697
+ const clearMilestoneValues = new Set(['', 'none', 'null', 'unassigned', 'clear']);
698
+
699
+ if (!clearMilestoneValues.has(milestoneRef.toLowerCase())) {
700
+ if (!resolvedProject?.id) {
701
+ throw new Error('Missing required field: project. Provide project when assigning milestone by name during issue create, or use projectMilestoneId.');
702
+ }
703
+
704
+ const milestone = await resolveMilestoneRef(client, milestoneRef, resolvedProject.id);
705
+ createInput.projectMilestoneId = milestone.id;
706
+ }
707
+ }
708
+
735
709
  const issue = await createIssue(client, createInput);
736
710
 
737
711
  const identifier = issue.identifier || issue.id || 'unknown';
@@ -742,8 +716,11 @@ export async function executeIssueCreate(client, params, options = {}) {
742
716
  const stateLabel = issue.state?.name || 'Unknown';
743
717
  const assigneeLabel = issue.assignee?.displayName || 'Unassigned';
744
718
 
719
+ const milestoneLabel = issue.projectMilestone?.name || null;
720
+
745
721
  const metaParts = [`Team: ${team.name}`, `Project: ${projectLabel}`, `State: ${stateLabel}`, `Assignee: ${assigneeLabel}`];
746
722
  if (priorityLabel) metaParts.push(`Priority: ${priorityLabel}`);
723
+ if (milestoneLabel) metaParts.push(`Milestone: ${milestoneLabel}`);
747
724
 
748
725
  return toTextResult(
749
726
  `Created issue **${identifier}**: ${issue.title}\n${metaParts.join(' | ')}`,
@@ -755,6 +732,7 @@ export async function executeIssueCreate(client, params, options = {}) {
755
732
  project: issue.project,
756
733
  state: issue.state,
757
734
  assignee: issue.assignee,
735
+ projectMilestone: issue.projectMilestone,
758
736
  url: issue.url,
759
737
  }
760
738
  );
package/src/linear.js CHANGED
@@ -3150,6 +3150,7 @@ export async function setIssueState(client, issueId, stateId) {
3150
3150
  * @param {string} [input.projectId] - Project ID
3151
3151
  * @param {number|string} [input.priority] - Issue priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low; or none/urgent/high/medium/low
3152
3152
  * @param {string} [input.assigneeId] - Assignee ID
3153
+ * @param {string} [input.projectMilestoneId] - Project milestone ID
3153
3154
  * @param {string} [input.parentId] - Parent issue ID for sub-issues
3154
3155
  * @returns {Promise<Object>} Created issue
3155
3156
  */
@@ -3202,6 +3203,10 @@ export async function createIssue(client, input) {
3202
3203
  createInput.stateId = input.stateId;
3203
3204
  }
3204
3205
 
3206
+ if (input.projectMilestoneId !== undefined) {
3207
+ createInput.projectMilestoneId = input.projectMilestoneId;
3208
+ }
3209
+
3205
3210
  if (getRawRequest(client)) {
3206
3211
  const payload = await executeGraphQL(client, ISSUE_CREATE_MUTATION, { input: createInput });
3207
3212
  if (!payload?.issueCreate?.success) {
@@ -3245,6 +3250,9 @@ export async function createIssue(client, input) {
3245
3250
  state: null,
3246
3251
  team: null,
3247
3252
  project: null,
3253
+ projectMilestone: createInput.projectMilestoneId
3254
+ ? { id: createInput.projectMilestoneId, name: 'Unknown' }
3255
+ : null,
3248
3256
  assignee: null,
3249
3257
  };
3250
3258
  }
@@ -3259,6 +3267,9 @@ export async function createIssue(client, input) {
3259
3267
  state: null,
3260
3268
  team: null,
3261
3269
  project: null,
3270
+ projectMilestone: createInput.projectMilestoneId
3271
+ ? { id: createInput.projectMilestoneId, name: 'Unknown' }
3272
+ : null,
3262
3273
  assignee: null,
3263
3274
  };
3264
3275
  }, 'createIssue');