@fink-andreas/pi-linear-tools 0.6.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,5 +1,36 @@
1
1
  # Changelog
2
2
 
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)
15
+
16
+ Issue image fetching and file download release.
17
+
18
+ ### New Features
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`.
23
+
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.
29
+
30
+ ### Documentation
31
+ - Added smoke test coverage for image and attachment workflows.
32
+ - Added dedicated release notes in `docs/RELEASE_NOTES_v0.7.0.md`.
33
+
3
34
  ## v0.6.0 (2026-04-23)
4
35
 
5
36
  Rate-limit resilience for milestone operations.
package/README.md CHANGED
@@ -41,6 +41,7 @@ Optional non-interactive commands:
41
41
  /linear-tools-config --api-key lin_xxx
42
42
  /linear-tools-config --default-team ENG
43
43
  /linear-tools-config --team ENG --project "My Project"
44
+ /linear-tools-config --allow-overwrite-files true|false
44
45
  ```
45
46
 
46
47
  ## Extension commands
@@ -67,7 +68,11 @@ Reference conventions:
67
68
  ## LLM-callable tools
68
69
 
69
70
  ### `linear_issue`
70
- Actions: `list`, `view`, `activity`, `create`, `update`, `comment`, `start`, `delete`
71
+ Actions: `list`, `view`, `images`, `download`, `activity`, `create`, `update`, `comment`, `start`, `delete`
72
+
73
+ `images` fetches image URLs embedded in issue markdown/comments and returns image content inline.
74
+
75
+ `download` fetches Linear issue attachments only. Destination directories must be relative paths. Existing files are not overwritten unless both `overwrite: true` is provided and the config guard is enabled with `/linear-tools-config --allow-overwrite-files true`.
71
76
 
72
77
  ### `linear_project`
73
78
  Actions: `list`, `view`, `create`, `update`, `delete`, `archive`, `unarchive`
@@ -94,11 +99,12 @@ pi-linear-tools config
94
99
  pi-linear-tools config --api-key lin_xxx
95
100
  pi-linear-tools config --default-team ENG
96
101
  pi-linear-tools config --team ENG --project "My Project"
102
+ pi-linear-tools config --allow-overwrite-files true|false
97
103
  ```
98
104
 
99
105
  ### Issue commands
100
106
 
101
- Use `update` to change the issue itself. Use `comment` to add discussion. Use `activity` to read the Activity timeline shown in Linear.
107
+ Use `update` to change the issue itself. Use `comment` to add discussion. Use `activity` to read the Activity timeline shown in Linear. Use `images` to fetch image attachments embedded in issue markdown/comments.
102
108
 
103
109
  ```bash
104
110
  # List issues
@@ -110,10 +116,18 @@ pi-linear-tools issue list --project "My Project" --assignee me
110
116
  pi-linear-tools issue view ENG-123
111
117
  pi-linear-tools issue view ENG-123 --no-comments
112
118
 
119
+ # Fetch markdown-embedded issue/comment images
120
+ pi-linear-tools issue images ENG-123
121
+ pi-linear-tools issue images ENG-123 --limit 5
122
+
113
123
  # Read Activity timeline
114
124
  pi-linear-tools issue activity ENG-123 --limit 20
115
125
  pi-linear-tools issue activity https://linear.app/workspace/issue/ENG-123/example --limit 20
116
126
 
127
+ # Download a Linear issue attachment
128
+ pi-linear-tools issue download ENG-123 --attachment-index 1 --directory downloads
129
+ pi-linear-tools issue download ENG-123 --attachment-id att_xxx --directory downloads --filename spec.pdf
130
+
117
131
  # Create issue
118
132
  pi-linear-tools issue create --title "Fix login bug" --team ENG
119
133
  pi-linear-tools issue create --title "New feature" --team ENG --project "My Project" --priority 2 --assignee me
@@ -124,6 +138,10 @@ pi-linear-tools issue update ENG-123 --title "Updated title" --assignee me
124
138
  pi-linear-tools issue update ENG-123 --milestone "Sprint 1"
125
139
  pi-linear-tools issue update ENG-123 --sub-issue-of ENG-100
126
140
 
141
+ # Issue priority uses Linear's native scale: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low.
142
+ # You can also use aliases: none, urgent, high, medium, low.
143
+ pi-linear-tools issue update ENG-123 --priority urgent
144
+
127
145
  # Comment on issue
128
146
  pi-linear-tools issue comment ENG-123 --body "This is fixed in PR #456"
129
147
  pi-linear-tools issue comment ENG-123 --body "Blocked on API review"
@@ -48,6 +48,8 @@ try {
48
48
  import {
49
49
  executeIssueList,
50
50
  executeIssueView,
51
+ executeIssueImages,
52
+ executeIssueDownload,
51
53
  executeIssueActivity,
52
54
  executeIssueCreate,
53
55
  executeIssueUpdate,
@@ -144,7 +146,9 @@ async function withRequestUsageLogging(client, toolName, action, operation, rate
144
146
  debug('[pi-linear-tools] Rate limit status', {
145
147
  tool: toolName,
146
148
  action,
149
+ requestsDelta: usageDelta.requestsDelta,
147
150
  rateLimitUsed: rateLimitInfo.used,
151
+ rateLimitTotal: rateLimitInfo.total,
148
152
  rateLimitRemaining: rateLimitInfo.remaining,
149
153
  rateLimitPercent: rateLimitInfo.usagePercent,
150
154
  rateLimitResetAt: rateLimitInfo.resetTime,
@@ -161,7 +165,10 @@ async function withRequestUsageLogging(client, toolName, action, operation, rate
161
165
 
162
166
  // Add rate limit info to details if debug is enabled
163
167
  if (rateLimitDebug && rateLimitInfo) {
164
- details.rateLimit = rateLimitInfo;
168
+ details.rateLimit = {
169
+ ...rateLimitInfo,
170
+ requestsDelta: usageDelta.requestsDelta,
171
+ };
165
172
  }
166
173
 
167
174
  if (!INCLUDE_USAGE_SUMMARY && !rateLimitDebug) {
@@ -181,8 +188,12 @@ async function withRequestUsageLogging(client, toolName, action, operation, rate
181
188
  }
182
189
 
183
190
  if (rateLimitDebug && rateLimitInfo) {
184
- const percent = rateLimitInfo.usagePercent;
185
- appendedText += `\n\n---\n**Rate Limit**: ${rateLimitInfo.used}/${rateLimitInfo.total} used (${percent}%) • Resets at ${rateLimitInfo.resetTime}`;
191
+ const percent = rateLimitInfo.usagePercent === null ? 'unknown' : `${rateLimitInfo.usagePercent}%`;
192
+ const windowUsage = rateLimitInfo.remaining === null
193
+ ? 'request window: unknown'
194
+ : `request window: ${rateLimitInfo.used}/${rateLimitInfo.total} used (${percent})`;
195
+ const reset = rateLimitInfo.resetTime ? ` • Resets at ${rateLimitInfo.resetTime}` : '';
196
+ appendedText += `\n\n---\n**Rate Limit**: +${usageDelta.requestsDelta} requests this call | ${windowUsage}${reset}`;
186
197
  }
187
198
 
188
199
  return {
@@ -213,7 +224,9 @@ async function withRequestUsageLogging(client, toolName, action, operation, rate
213
224
  rateLimitedDelta: after.rateLimited - before.rateLimited,
214
225
  error: String(error?.message || error || 'unknown'),
215
226
  ...(rateLimitDebug && rateLimitInfo ? {
227
+ requestsDelta: after.total - before.total,
216
228
  rateLimitUsed: rateLimitInfo.used,
229
+ rateLimitTotal: rateLimitInfo.total,
217
230
  rateLimitRemaining: rateLimitInfo.remaining,
218
231
  rateLimitPercent: rateLimitInfo.usagePercent,
219
232
  } : {}),
@@ -345,7 +358,7 @@ async function runInteractiveConfigFlow(ctx, pi) {
345
358
  }
346
359
 
347
360
  if (!client) {
348
- 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']);
349
362
 
350
363
  if (!selectedAuthMethod) {
351
364
  ctx.ui.notify('Configuration cancelled', 'warning');
@@ -629,18 +642,18 @@ async function registerLinearTools(pi) {
629
642
  name: 'linear_issue',
630
643
  label: 'Linear Issue',
631
644
  description: 'Interact with Linear issues.',
632
- promptSnippet: 'Interact with Linear issues (list, view, activity, create, update, comment, start, delete)',
645
+ promptSnippet: 'Interact with Linear issues (list, view, images, download, activity, create, update, comment, start, delete)',
633
646
  parameters: {
634
647
  type: 'object',
635
648
  properties: {
636
649
  action: {
637
650
  type: 'string',
638
- enum: ['list', 'view', 'activity', 'create', 'update', 'comment', 'start', 'delete'],
651
+ enum: ['list', 'view', 'images', 'download', 'activity', 'create', 'update', 'comment', 'start', 'delete'],
639
652
  description: 'Action to perform on issue(s)',
640
653
  },
641
654
  issue: {
642
655
  type: 'string',
643
- description: 'Issue key (ABC-123) or Linear issue ID (for view, activity, update, comment, start, delete)',
656
+ description: 'Issue key (ABC-123) or Linear issue ID (for view, images, download, activity, update, comment, start, delete)',
644
657
  },
645
658
  project: {
646
659
  type: 'string',
@@ -661,11 +674,46 @@ async function registerLinearTools(pi) {
661
674
  },
662
675
  limit: {
663
676
  type: 'integer',
664
- description: 'Maximum number of issues or activity entries to list',
677
+ description: 'Maximum number of issues, activity entries, or images to fetch',
665
678
  },
666
679
  includeComments: {
667
680
  type: 'boolean',
668
- description: 'Include comments when viewing issue (default: true)',
681
+ description: 'Include comments when viewing an issue or fetching images (default: true)',
682
+ },
683
+ attachmentId: {
684
+ type: 'string',
685
+ description: 'Attachment ID to download (for download). Use exactly one attachment selector when multiple attachments exist.',
686
+ },
687
+ attachmentTitle: {
688
+ type: 'string',
689
+ description: 'Attachment title to download (for download). Must uniquely match an issue attachment.',
690
+ },
691
+ attachmentUrl: {
692
+ type: 'string',
693
+ description: 'Attachment URL to download (for download). Must uniquely match an issue attachment.',
694
+ },
695
+ attachmentIndex: {
696
+ type: 'integer',
697
+ description: '1-based attachment index from the issue attachment list (for download).',
698
+ minimum: 1,
699
+ },
700
+ directory: {
701
+ type: 'string',
702
+ description: 'Relative destination directory for attachment downloads. Missing directories are created.',
703
+ },
704
+ filename: {
705
+ type: 'string',
706
+ description: 'Optional output filename for attachment downloads. Unsafe characters are sanitized.',
707
+ },
708
+ overwrite: {
709
+ type: 'boolean',
710
+ description: 'Overwrite existing file during download (default: false). Requires allow_overwrite_files=true in config.',
711
+ },
712
+ maxBytes: {
713
+ type: 'integer',
714
+ description: 'Maximum bytes to fetch. For images, applies per image with a 10 MiB handler limit. For download, default/max is 52428800.',
715
+ minimum: 1,
716
+ maximum: 52428800,
669
717
  },
670
718
  title: {
671
719
  type: 'string',
@@ -676,8 +724,19 @@ async function registerLinearTools(pi) {
676
724
  description: 'Issue description in markdown (for create, update)',
677
725
  },
678
726
  priority: {
679
- type: 'integer',
680
- description: 'Priority 0..4 (for create, update)',
727
+ description: 'Issue priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low (Linear-native scale), or one of: none, urgent, high, medium, low (for create/update)',
728
+ oneOf: [
729
+ {
730
+ type: 'integer',
731
+ minimum: 0,
732
+ maximum: 4,
733
+ multipleOf: 1,
734
+ },
735
+ {
736
+ type: 'string',
737
+ enum: ['none', 'urgent', 'high', 'medium', 'low'],
738
+ },
739
+ ],
681
740
  },
682
741
  includeArchived: {
683
742
  type: 'boolean',
@@ -689,11 +748,11 @@ async function registerLinearTools(pi) {
689
748
  },
690
749
  milestone: {
691
750
  type: 'string',
692
- 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.',
693
752
  },
694
753
  projectMilestoneId: {
695
754
  type: 'string',
696
- description: 'Optional explicit milestone ID alias for update.',
755
+ description: 'Optional explicit milestone ID alias for create/update.',
697
756
  },
698
757
  subIssueOf: {
699
758
  type: 'string',
@@ -726,6 +785,7 @@ async function registerLinearTools(pi) {
726
785
  estimate: {
727
786
  type: 'integer',
728
787
  description: 'Estimate/story points for the issue (non-negative integer, for create/update)',
788
+ minimum: 0,
729
789
  },
730
790
  team: {
731
791
  type: 'string',
@@ -775,6 +835,10 @@ async function registerLinearTools(pi) {
775
835
  return await executeIssueList(client, params);
776
836
  case 'view':
777
837
  return await executeIssueView(client, params);
838
+ case 'images':
839
+ return await executeIssueImages(client, params);
840
+ case 'download':
841
+ return await executeIssueDownload(client, params, { settings });
778
842
  case 'activity':
779
843
  return await executeIssueActivity(client, params);
780
844
  case 'create':
@@ -862,6 +926,12 @@ async function registerLinearTools(pi) {
862
926
  renderResult: renderMarkdownResult,
863
927
  async execute(_toolCallId, params) {
864
928
  return executeToolSafely('Linear project operation failed', async () => {
929
+ // Pre-check: skip API calls if we know we're rate limited
930
+ const { isRateLimited, resetAt } = checkAndClearRateLimit();
931
+ if (isRateLimited) {
932
+ return buildRateLimitToolResult({ requestsResetAt: resetAt.getTime(), type: 'Ratelimited' }, { cached: true });
933
+ }
934
+
865
935
  const settings = await loadSettings();
866
936
  const rateLimitDebug = settings.rateLimitDebug || false;
867
937
  const client = await createAuthenticatedClient();
@@ -941,6 +1011,12 @@ async function registerLinearTools(pi) {
941
1011
  renderResult: renderMarkdownResult,
942
1012
  async execute(_toolCallId, params) {
943
1013
  return executeToolSafely('Linear project update operation failed', async () => {
1014
+ // Pre-check: skip API calls if we know we're rate limited
1015
+ const { isRateLimited, resetAt } = checkAndClearRateLimit();
1016
+ if (isRateLimited) {
1017
+ return buildRateLimitToolResult({ requestsResetAt: resetAt.getTime(), type: 'Ratelimited' }, { cached: true });
1018
+ }
1019
+
944
1020
  const settings = await loadSettings();
945
1021
  const rateLimitDebug = settings.rateLimitDebug || false;
946
1022
  const client = await createAuthenticatedClient();
@@ -1083,7 +1159,7 @@ export default async function piLinearToolsExtension(pi) {
1083
1159
  // Safety wrapper: never let extension errors crash pi
1084
1160
  try {
1085
1161
  pi.registerCommand('linear-tools-config', {
1086
- description: 'Configure pi-linear-tools settings (API key, default team, rate limit debug)',
1162
+ description: 'Configure pi-linear-tools settings (API key, default team, rate limit debug, file overwrite guard)',
1087
1163
  handler: async (argsText, ctx) => {
1088
1164
  const args = parseArgs(argsText);
1089
1165
  const apiKey = readFlag(args, '--api-key');
@@ -1091,6 +1167,7 @@ export default async function piLinearToolsExtension(pi) {
1091
1167
  const projectTeam = readFlag(args, '--team');
1092
1168
  const projectName = readFlag(args, '--project');
1093
1169
  const rateLimitDebug = readFlag(args, '--rate-limit-debug');
1170
+ const allowOverwriteFiles = readFlag(args, '--allow-overwrite-files');
1094
1171
 
1095
1172
  if (apiKey) {
1096
1173
  const settings = await loadSettings();
@@ -1122,6 +1199,17 @@ export default async function piLinearToolsExtension(pi) {
1122
1199
  return;
1123
1200
  }
1124
1201
 
1202
+ if (allowOverwriteFiles) {
1203
+ const settings = await loadSettings();
1204
+ const enabled = allowOverwriteFiles === 'true' || allowOverwriteFiles === '1';
1205
+ settings.allow_overwrite_files = enabled;
1206
+ await saveSettings(settings);
1207
+ if (ctx?.hasUI) {
1208
+ ctx.ui.notify(`File overwrite guard ${enabled ? 'allows overwrites' : 'blocks overwrites'}`, 'info');
1209
+ }
1210
+ return;
1211
+ }
1212
+
1125
1213
  if (defaultTeam) {
1126
1214
  const settings = await loadSettings();
1127
1215
  settings.defaultTeam = defaultTeam;
@@ -1165,13 +1253,13 @@ export default async function piLinearToolsExtension(pi) {
1165
1253
  return;
1166
1254
  }
1167
1255
 
1168
- if (!apiKey && !defaultTeam && !projectTeam && !projectName && !rateLimitDebug && ctx?.hasUI && ctx?.ui) {
1256
+ if (!apiKey && !defaultTeam && !projectTeam && !projectName && !rateLimitDebug && !allowOverwriteFiles && ctx?.hasUI && ctx?.ui) {
1169
1257
  await runInteractiveConfigFlow(ctx, pi);
1170
1258
  return;
1171
1259
  }
1172
1260
 
1173
1261
  // Show current settings if no valid action was specified
1174
- if (apiKey || defaultTeam || projectTeam || projectName || rateLimitDebug) {
1262
+ if (apiKey || defaultTeam || projectTeam || projectName || rateLimitDebug || allowOverwriteFiles) {
1175
1263
  // Actions above already handled and returned
1176
1264
  return;
1177
1265
  }
@@ -1182,7 +1270,7 @@ export default async function piLinearToolsExtension(pi) {
1182
1270
 
1183
1271
  pi.sendMessage({
1184
1272
  customType: 'pi-linear-tools',
1185
- content: `Configuration:\n LINEAR_API_KEY: ${hasKey ? 'configured' : 'not set'} (source: ${keySource})\n Default workspace: ${settings.defaultWorkspace?.name || 'not set'}\n Default team: ${settings.defaultTeam || 'not set'}\n Rate limit debug: ${settings.rateLimitDebug ? 'enabled' : 'disabled'}\n Project team mappings: ${Object.keys(settings.projects || {}).length}\n\nCommands:\n /linear-tools-config --api-key lin_xxx\n /linear-tools-config --default-team ENG\n /linear-tools-config --team ENG --project MyProject\n /linear-tools-config --rate-limit-debug true|false\n\nNote: environment LINEAR_API_KEY takes precedence over settings file.`,
1273
+ content: `Configuration:\n LINEAR_API_KEY: ${hasKey ? 'configured' : 'not set'} (source: ${keySource})\n Default workspace: ${settings.defaultWorkspace?.name || 'not set'}\n Default team: ${settings.defaultTeam || 'not set'}\n Rate limit debug: ${settings.rateLimitDebug ? 'enabled' : 'disabled'}\n Allow overwrite files: ${settings.allow_overwrite_files ? 'enabled' : 'disabled'}\n Project team mappings: ${Object.keys(settings.projects || {}).length}\n\nCommands:\n /linear-tools-config --api-key lin_xxx\n /linear-tools-config --default-team ENG\n /linear-tools-config --team ENG --project MyProject\n /linear-tools-config --rate-limit-debug true|false\n /linear-tools-config --allow-overwrite-files true|false\n\nNote: environment LINEAR_API_KEY takes precedence over settings file.`,
1186
1274
  display: true,
1187
1275
  });
1188
1276
  },
@@ -1208,7 +1296,7 @@ export default async function piLinearToolsExtension(pi) {
1208
1296
  const showMilestoneTool = await shouldExposeMilestoneTool();
1209
1297
  const toolLines = [
1210
1298
  'LLM-callable tools:',
1211
- ' linear_issue (list/view/activity/create/update/comment/start/delete)',
1299
+ ' linear_issue (list/view/images/download/activity/create/update/comment/start/delete)',
1212
1300
  ' linear_project (list/view/create/update/delete/archive/unarchive)',
1213
1301
  ' linear_project_update (list/view/create/update/archive/unarchive)',
1214
1302
  ' linear_team (list)',
@@ -1228,6 +1316,7 @@ export default async function piLinearToolsExtension(pi) {
1228
1316
  ' /linear-tools-config --default-team <team-key>',
1229
1317
  ' /linear-tools-config --team <team-key> --project <project-name-or-id>',
1230
1318
  ' /linear-tools-config --rate-limit-debug true|false',
1319
+ ' /linear-tools-config --allow-overwrite-files true|false',
1231
1320
  ' /linear-tools-help',
1232
1321
  ' /linear-tools-reload',
1233
1322
  '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fink-andreas/pi-linear-tools",
3
- "version": "0.6.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-settings.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
  },
@@ -1,7 +1,11 @@
1
1
  {
2
- "schemaVersion": 1,
3
- "linearApiKey": "lin_xxx",
2
+ "schemaVersion": 2,
3
+ "authMethod": "api-key",
4
+ "apiKey": "lin_xxx",
4
5
  "defaultTeam": "ENG",
6
+ "defaultWorkspace": null,
7
+ "rateLimitDebug": false,
8
+ "allow_overwrite_files": false,
5
9
  "projects": {
6
10
  "97ec7cae-e252-493d-94d3-6910aa28cacf": {
7
11
  "scope": {
package/src/cli.js CHANGED
@@ -18,6 +18,8 @@ import {
18
18
  import {
19
19
  executeIssueList,
20
20
  executeIssueView,
21
+ executeIssueImages,
22
+ executeIssueDownload,
21
23
  executeIssueActivity,
22
24
  executeIssueCreate,
23
25
  executeIssueUpdate,
@@ -164,6 +166,8 @@ Other commands:
164
166
  config Show current configuration
165
167
  config --api-key <key> Set Linear API key (legacy)
166
168
  config --default-team <key> Set default team
169
+ config --allow-overwrite-files true|false
170
+ Allow issue download overwrites when requested
167
171
 
168
172
  Auth Actions:
169
173
  login Authenticate with Linear via OAuth 2.0
@@ -173,9 +177,12 @@ Auth Actions:
173
177
  Issue Actions:
174
178
  list [--project X] [--states X,Y] [--assignee me|all] [--team X] [--limit N]
175
179
  view <issue> [--no-comments]
180
+ images <issue> [--no-comments] [--limit N] [--max-bytes N]
181
+ download <issue> --directory DIR [--attachment-id ID|--attachment-title TITLE|--attachment-url URL|--attachment-index N]
182
+ [--filename NAME] [--overwrite true|false] [--max-bytes N]
176
183
  activity <issue> [--limit N] [--include-archived true|false]
177
- create --title X [--team X] [--project X] [--description X] [--priority 0-4] [--assignee me|ID]
178
- update <issue> [--title X] [--description X] [--state X] [--priority 0-4]
184
+ create --title X [--team X] [--project X] [--description X] [--priority 0-4|name] [--assignee me|ID]
185
+ update <issue> [--title X] [--description X] [--state X] [--priority 0-4|name]
179
186
  [--assignee me|ID] [--milestone X] [--sub-issue-of X]
180
187
  comment <issue> --body X
181
188
  start <issue> [--from-ref X] [--on-branch-exists switch|suffix]
@@ -242,7 +249,7 @@ Common Flags:
242
249
  --project Project name or ID
243
250
  --team Team key (e.g., ENG)
244
251
  --assignee "me" or assignee ID
245
- --priority Priority 0-4 (0=None, 1=Urgent, 2=High, 3=Medium, 4=Low)
252
+ --priority Issue priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low; or none, urgent, high, medium, low
246
253
  --state State name or ID
247
254
  --limit Max results (default: 50)
248
255
 
@@ -286,11 +293,14 @@ Guidance:
286
293
  Use "update" to change the issue itself: title, description, state, assignee, milestone, or parent.
287
294
  Use "comment" to add a discussion entry.
288
295
  Use "activity" to read the Activity timeline shown in Linear.
296
+ Use "images" to fetch image attachments embedded in issue markdown/comments.
289
297
 
290
298
  Actions:
291
299
  list List issues in a project
292
300
  view View issue details
301
+ images Fetch image attachments embedded in issue markdown/comments
293
302
  activity View issue activity/history
303
+ download Download a Linear issue attachment
294
304
  create Create a new issue
295
305
  update Update an existing issue
296
306
  comment Add a comment to an issue
@@ -308,6 +318,23 @@ View Options:
308
318
  <issue> Issue key (e.g., ENG-123), ID, or issue URL
309
319
  --no-comments Exclude comments from output
310
320
 
321
+ Images Options:
322
+ <issue> Issue key (e.g., ENG-123), ID, or issue URL
323
+ --no-comments Exclude images from comments
324
+ --limit N Max images to fetch (default: 10)
325
+ --max-bytes N Max bytes per image (default/max: 10485760)
326
+
327
+ Download Options:
328
+ <issue> Issue key, ID, or issue URL
329
+ --directory DIR Relative destination directory (required)
330
+ --attachment-id ID Attachment ID selector
331
+ --attachment-title X Attachment title selector (must be unique)
332
+ --attachment-url URL Attachment URL selector
333
+ --attachment-index N 1-based attachment index selector
334
+ --filename NAME Optional output filename
335
+ --overwrite X true or false (default: false; requires config guard)
336
+ --max-bytes N Max download bytes (default/max: 52428800)
337
+
311
338
  Activity Options:
312
339
  <issue> Issue key, ID, or issue URL
313
340
  --limit N Max activity entries to fetch (default: 20)
@@ -318,7 +345,7 @@ Create Options:
318
345
  --team X Team key, e.g., ENG (required if no default team)
319
346
  --project X Project name or ID
320
347
  --description X Issue description (markdown)
321
- --priority N Priority 0-4
348
+ --priority N Issue priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low; or none, urgent, high, medium, low
322
349
  --assignee X "me" or assignee ID
323
350
  --parent-id X Parent issue ID for sub-issues
324
351
 
@@ -327,7 +354,7 @@ Update Options:
327
354
  --title X New title
328
355
  --description X New description
329
356
  --state X New state name or ID
330
- --priority N New priority 0-4
357
+ --priority N New issue priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low; or none, urgent, high, medium, low
331
358
  --assignee X "me" or assignee ID
332
359
  --milestone X Milestone name/ID, or "none" to clear
333
360
  --sub-issue-of X Parent issue key/ID, or "none" to clear
@@ -351,6 +378,7 @@ Examples:
351
378
  pi-linear-tools issue comment ENG-123 --body "Ready for review"
352
379
  pi-linear-tools issue activity ENG-123 --limit 20
353
380
  pi-linear-tools issue activity https://linear.app/workspace/issue/ENG-123/example --limit 20
381
+ pi-linear-tools issue download ENG-123 --attachment-index 1 --directory downloads
354
382
  `);
355
383
  }
356
384
 
@@ -729,6 +757,7 @@ async function handleConfig(args) {
729
757
  const defaultTeam = readFlag(args, '--default-team');
730
758
  const projectTeam = readFlag(args, '--team');
731
759
  const projectName = readFlag(args, '--project');
760
+ const allowOverwriteFiles = readFlag(args, '--allow-overwrite-files');
732
761
 
733
762
  if (apiKey) {
734
763
  const settings = await loadSettings();
@@ -748,6 +777,19 @@ async function handleConfig(args) {
748
777
  return;
749
778
  }
750
779
 
780
+ if (allowOverwriteFiles !== undefined) {
781
+ const enabled = parseBoolean(allowOverwriteFiles);
782
+ if (enabled === undefined) {
783
+ throw new Error('Invalid value for --allow-overwrite-files. Use true or false.');
784
+ }
785
+
786
+ const settings = await loadSettings();
787
+ settings.allow_overwrite_files = enabled;
788
+ await saveSettings(settings);
789
+ console.log(`File overwrite guard ${enabled ? 'allows overwrites' : 'blocks overwrites'}`);
790
+ return;
791
+ }
792
+
751
793
  if (projectTeam) {
752
794
  if (!projectName) {
753
795
  throw new Error('Missing required flag: --project when using --team');
@@ -777,12 +819,14 @@ async function handleConfig(args) {
777
819
  console.log(`Configuration:
778
820
  LINEAR_API_KEY: ${hasKey ? 'configured' : 'not set'} (source: ${keySource})
779
821
  Default team: ${settings.defaultTeam || 'not set'}
822
+ Allow overwrite files: ${settings.allow_overwrite_files ? 'enabled' : 'disabled'}
780
823
  Project team mappings: ${Object.keys(settings.projects || {}).length}
781
824
 
782
825
  Commands:
783
826
  pi-linear-tools config --api-key lin_xxx
784
827
  pi-linear-tools config --default-team ENG
785
- pi-linear-tools config --team ENG --project MyProject`);
828
+ pi-linear-tools config --team ENG --project MyProject
829
+ pi-linear-tools config --allow-overwrite-files true|false`);
786
830
  }
787
831
 
788
832
  // ===== ISSUE HANDLERS =====
@@ -819,6 +863,25 @@ async function handleIssueView(args) {
819
863
  console.log(result.content[0].text);
820
864
  }
821
865
 
866
+ async function handleIssueImages(args) {
867
+ const client = await createAuthenticatedClient();
868
+
869
+ const positional = args.filter((a) => !a.startsWith('-'));
870
+ if (positional.length === 0) {
871
+ throw new Error('Missing required argument: issue key or ID');
872
+ }
873
+
874
+ const params = {
875
+ issue: positional[0],
876
+ includeComments: !hasFlag(args, '--no-comments'),
877
+ limit: parseNumber(readFlag(args, '--limit')),
878
+ maxBytes: parseNumber(readFlag(args, '--max-bytes')),
879
+ };
880
+
881
+ const result = await executeIssueImages(client, params);
882
+ console.log(result.content[0].text);
883
+ }
884
+
822
885
  async function handleIssueActivity(args) {
823
886
  const client = await createAuthenticatedClient();
824
887
 
@@ -837,6 +900,32 @@ async function handleIssueActivity(args) {
837
900
  console.log(result.content[0].text);
838
901
  }
839
902
 
903
+ async function handleIssueDownload(args) {
904
+ const client = await createAuthenticatedClient();
905
+ const settings = await loadSettings();
906
+
907
+ const positional = args.filter((a) => !a.startsWith('-'));
908
+ if (positional.length === 0) {
909
+ throw new Error('Missing required argument: issue key or ID');
910
+ }
911
+
912
+ const overwrite = parseBoolean(readFlag(args, '--overwrite'));
913
+ const params = {
914
+ issue: positional[0],
915
+ attachmentId: readFlag(args, '--attachment-id'),
916
+ attachmentTitle: readFlag(args, '--attachment-title'),
917
+ attachmentUrl: readFlag(args, '--attachment-url'),
918
+ attachmentIndex: parseNumber(readFlag(args, '--attachment-index')),
919
+ directory: readFlag(args, '--directory'),
920
+ filename: readFlag(args, '--filename'),
921
+ overwrite: overwrite === undefined ? undefined : overwrite,
922
+ maxBytes: parseNumber(readFlag(args, '--max-bytes')),
923
+ };
924
+
925
+ const result = await executeIssueDownload(client, params, { settings });
926
+ console.log(result.content[0].text);
927
+ }
928
+
840
929
  async function handleIssueCreate(args) {
841
930
  const client = await createAuthenticatedClient();
842
931
 
@@ -845,7 +934,7 @@ async function handleIssueCreate(args) {
845
934
  team: readFlag(args, '--team'),
846
935
  project: readFlag(args, '--project'),
847
936
  description: readFlag(args, '--description'),
848
- priority: parseNumber(readFlag(args, '--priority')),
937
+ priority: readFlag(args, '--priority'),
849
938
  assignee: readFlag(args, '--assignee'),
850
939
  parentId: readFlag(args, '--parent-id'),
851
940
  state: readFlag(args, '--state'),
@@ -872,7 +961,7 @@ async function handleIssueUpdate(args) {
872
961
  title: readFlag(args, '--title'),
873
962
  description: readFlag(args, '--description'),
874
963
  state: readFlag(args, '--state'),
875
- priority: parseNumber(readFlag(args, '--priority')),
964
+ priority: readFlag(args, '--priority'),
876
965
  assignee: readFlag(args, '--assignee'),
877
966
  milestone: readFlag(args, '--milestone'),
878
967
  subIssueOf: readFlag(args, '--sub-issue-of'),
@@ -950,6 +1039,10 @@ async function handleIssue(args) {
950
1039
  return handleIssueList(rest);
951
1040
  case 'view':
952
1041
  return handleIssueView(rest);
1042
+ case 'images':
1043
+ return handleIssueImages(rest);
1044
+ case 'download':
1045
+ return handleIssueDownload(rest);
953
1046
  case 'activity':
954
1047
  return handleIssueActivity(rest);
955
1048
  case 'create':