@fink-andreas/pi-linear-tools 0.7.1 → 0.7.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/CHANGELOG.md +25 -0
- package/extensions/pi-linear-tools.js +54 -19
- package/package.json +2 -2
- package/src/auth/constants.js +7 -2
- package/src/error-hints.js +27 -0
- package/src/handlers.js +7 -1
- package/src/shared.js +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.7.3 (2026-07-28)
|
|
4
|
+
|
|
5
|
+
Patch release that fixes Linear issue-relation updates for OAuth users.
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
- **Enable issue relations with default OAuth**: New OAuth grants now request Linear's `write` scope, allowing `linear_issue update` to set `blockedBy`, `blocking`, `relatedTo`, and `duplicateOf` relations.
|
|
9
|
+
- **Explain legacy-token remediation**: Relation updates rejected because `write` is missing now direct OAuth users to run `pi-linear-tools auth login`; API-key users are told to use a key with write access.
|
|
10
|
+
|
|
11
|
+
### Tests
|
|
12
|
+
- Added regression coverage for the default OAuth authorization URL scope and issue-relation scope guidance.
|
|
13
|
+
|
|
14
|
+
### Upgrade note
|
|
15
|
+
Existing OAuth tokens cannot gain the newly requested scope automatically. Re-authenticate with `pi-linear-tools auth login` before creating issue relations.
|
|
16
|
+
|
|
17
|
+
## v0.7.2 (2026-07-21)
|
|
18
|
+
|
|
19
|
+
Patch release for Pi package rename compatibility and terminal-safe fallback rendering.
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
- **Support renamed Pi packages**: The extension now supports both legacy `@mariozechner/*` imports and the current `@earendil-works/*` package scope, including globally installed Pi paths.
|
|
23
|
+
- **Prevent CJK fallback-rendering crashes**: Plain-text fallback output now truncates by terminal columns, preventing over-width lines from CJK/non-ASCII text and tabs from triggering Pi TUI's width guard.
|
|
24
|
+
|
|
25
|
+
### Tests
|
|
26
|
+
- Added fallback-renderer regression coverage for CJK text, tabs, unavailable Markdown dependencies, and Markdown rendering failures.
|
|
27
|
+
|
|
3
28
|
## v0.7.1 (2026-06-09)
|
|
4
29
|
|
|
5
30
|
Patch release for issue creation milestone assignment and configuration text cleanup.
|
|
@@ -12,7 +12,11 @@ async function importPiCodingAgent() {
|
|
|
12
12
|
try {
|
|
13
13
|
return await import('@mariozechner/pi-coding-agent');
|
|
14
14
|
} catch {
|
|
15
|
-
|
|
15
|
+
try {
|
|
16
|
+
return await import('@earendil-works/pi-coding-agent');
|
|
17
|
+
} catch {
|
|
18
|
+
return importFromPiRoot('dist/index.js');
|
|
19
|
+
}
|
|
16
20
|
}
|
|
17
21
|
}
|
|
18
22
|
|
|
@@ -20,8 +24,16 @@ async function importPiTui() {
|
|
|
20
24
|
try {
|
|
21
25
|
return await import('@mariozechner/pi-tui');
|
|
22
26
|
} catch {
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
try {
|
|
28
|
+
return await import('@earendil-works/pi-tui');
|
|
29
|
+
} catch {
|
|
30
|
+
// pi-tui is a dependency of pi-coding-agent and may be nested under it
|
|
31
|
+
try {
|
|
32
|
+
return await importFromPiRoot('node_modules/@mariozechner/pi-tui/dist/index.js');
|
|
33
|
+
} catch {
|
|
34
|
+
return importFromPiRoot('node_modules/@earendil-works/pi-tui/dist/index.js');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
25
37
|
}
|
|
26
38
|
}
|
|
27
39
|
|
|
@@ -602,33 +614,56 @@ async function executeToolSafely(operationLabel, operation, options = {}) {
|
|
|
602
614
|
}
|
|
603
615
|
}
|
|
604
616
|
|
|
605
|
-
|
|
617
|
+
// Conservative column width used only by the plain-text fallback renderer:
|
|
618
|
+
// printable ASCII counts as 1 column, tabs as 3 (matching pi-tui), and every
|
|
619
|
+
// other code point as 2. This overestimates narrow non-ASCII characters but
|
|
620
|
+
// never underestimates terminal display width, so rendered lines stay within
|
|
621
|
+
// the terminal width and pi-tui's over-width guard never fires.
|
|
622
|
+
function fallbackColumnWidth(codePoint) {
|
|
623
|
+
if (codePoint === 0x09) return 3;
|
|
624
|
+
return codePoint >= 0x20 && codePoint <= 0x7e ? 1 : 2;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function truncateLineToColumns(line, maxColumns) {
|
|
628
|
+
let columns = 0;
|
|
629
|
+
let end = 0;
|
|
630
|
+
for (const char of line) {
|
|
631
|
+
const width = fallbackColumnWidth(char.codePointAt(0));
|
|
632
|
+
if (columns + width > maxColumns) break;
|
|
633
|
+
columns += width;
|
|
634
|
+
end += char.length;
|
|
635
|
+
}
|
|
636
|
+
return line.slice(0, end);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export function createPlainTextFallbackRenderer(text) {
|
|
640
|
+
const lines = text.split('\n');
|
|
641
|
+
return {
|
|
642
|
+
render: (width) => lines.map((line) => (width ? truncateLineToColumns(line, width) : line)),
|
|
643
|
+
invalidate: () => {},
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export function renderMarkdownResult(result, _options, _theme, renderer = { Markdown, Text, getMarkdownTheme }) {
|
|
606
648
|
const text = result.content?.[0]?.text || '';
|
|
649
|
+
const { Markdown: markdown, Text: textComponent, getMarkdownTheme: getTheme } = renderer;
|
|
607
650
|
|
|
608
651
|
// Fall back to plain text if markdown packages not available
|
|
609
|
-
if (!
|
|
610
|
-
|
|
611
|
-
return {
|
|
612
|
-
render: (width) => lines.map((line) => (width && line.length > width ? line.slice(0, width) : line)),
|
|
613
|
-
invalidate: () => {},
|
|
614
|
-
};
|
|
652
|
+
if (!markdown || !getTheme) {
|
|
653
|
+
return createPlainTextFallbackRenderer(text);
|
|
615
654
|
}
|
|
616
655
|
|
|
617
656
|
// Return Markdown component directly - the TUI will call its render() method
|
|
618
657
|
try {
|
|
619
|
-
const mdTheme =
|
|
620
|
-
return new
|
|
658
|
+
const mdTheme = getTheme();
|
|
659
|
+
return new markdown(text, 0, 0, mdTheme, _theme ? { color: (t) => _theme.fg('toolOutput', t) } : undefined);
|
|
621
660
|
} catch (error) {
|
|
622
661
|
// If markdown rendering fails for any reason, show a visible error so we can diagnose.
|
|
623
662
|
const msg = `[pi-linear-tools] Markdown render failed: ${String(error?.message || error)}`;
|
|
624
|
-
if (
|
|
625
|
-
return new
|
|
663
|
+
if (textComponent) {
|
|
664
|
+
return new textComponent((_theme ? _theme.fg('error', msg) : msg) + `\n\n` + text, 0, 0);
|
|
626
665
|
}
|
|
627
|
-
|
|
628
|
-
return {
|
|
629
|
-
render: (width) => lines.map((line) => (width && line.length > width ? line.slice(0, width) : line)),
|
|
630
|
-
invalidate: () => {},
|
|
631
|
-
};
|
|
666
|
+
return createPlainTextFallbackRenderer(msg + '\n\n' + text);
|
|
632
667
|
}
|
|
633
668
|
}
|
|
634
669
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fink-andreas/pi-linear-tools",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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-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",
|
|
30
|
+
"test": "node tests/test-package-manifest.js && node tests/test-oauth.js && node tests/test-extension-registration.js && node tests/test-render-fallback.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/auth/constants.js
CHANGED
|
@@ -5,5 +5,10 @@
|
|
|
5
5
|
// Linear OAuth application client ID
|
|
6
6
|
export const OAUTH_CLIENT_ID = 'a3e177176c6697611367f1a2405d4a34';
|
|
7
7
|
|
|
8
|
-
// OAuth scopes - minimal required scopes
|
|
9
|
-
|
|
8
|
+
// OAuth scopes - minimal required scopes.
|
|
9
|
+
// `write` is included so issue relations (blockedBy / blocking / relatedTo / duplicateOf) work:
|
|
10
|
+
// those go through the `issueRelationCreate` mutation, which Linear gates behind the `write`
|
|
11
|
+
// scope, whereas `issueUpdate` (title/description/state/assignee/parentId) is permitted under
|
|
12
|
+
// `issues:create`. Without `write`, the `linear_issue` tool accepts the relation params but
|
|
13
|
+
// Linear rejects them with "Invalid scope: `write` required". See upstream issue #27.
|
|
14
|
+
export const OAUTH_SCOPES = ['read', 'issues:create', 'comments:create', 'write'];
|
package/src/error-hints.js
CHANGED
|
@@ -22,3 +22,30 @@ export function withMilestoneScopeHint(error) {
|
|
|
22
22
|
|
|
23
23
|
return error;
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Adds remediation guidance when an issue-relation mutation is rejected because
|
|
28
|
+
* an existing credential was issued before the default OAuth scopes included `write`.
|
|
29
|
+
*
|
|
30
|
+
* @param {Error} error - The error to wrap
|
|
31
|
+
* @param {object} patch - Requested issue update fields
|
|
32
|
+
* @returns {Error} The original error or a new error with additional hint
|
|
33
|
+
*/
|
|
34
|
+
export function withIssueRelationScopeHint(error, patch = {}) {
|
|
35
|
+
const message = String(error?.message || error || 'Unknown error');
|
|
36
|
+
const relationFields = ['blockedBy', 'blocking', 'relatedTo', 'duplicateOf'];
|
|
37
|
+
const requestsRelation = relationFields.some((field) => {
|
|
38
|
+
const value = patch[field];
|
|
39
|
+
return Array.isArray(value) ? value.length > 0 : String(value || '').trim() !== '';
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (requestsRelation && /invalid scope/i.test(message) && /write/i.test(message)) {
|
|
43
|
+
return new Error(
|
|
44
|
+
`${message}\nHint: Issue relations require Linear's write scope. ` +
|
|
45
|
+
`If you authenticated with OAuth before this scope was added, run ` +
|
|
46
|
+
'`pi-linear-tools auth login` to re-authenticate. API-key users need a key with write access.'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return error;
|
|
51
|
+
}
|
package/src/handlers.js
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
withHandlerErrorHandling,
|
|
47
47
|
getViewer,
|
|
48
48
|
} from './linear.js';
|
|
49
|
+
import { withIssueRelationScopeHint } from './error-hints.js';
|
|
49
50
|
import { debug } from './logger.js';
|
|
50
51
|
|
|
51
52
|
function toTextResult(text, details = {}) {
|
|
@@ -804,7 +805,12 @@ export async function executeIssueUpdate(client, params) {
|
|
|
804
805
|
projectMilestoneId: updatePatch.projectMilestoneId,
|
|
805
806
|
});
|
|
806
807
|
|
|
807
|
-
|
|
808
|
+
let result;
|
|
809
|
+
try {
|
|
810
|
+
result = await updateIssue(client, issue, updatePatch);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
throw withIssueRelationScopeHint(error, updatePatch);
|
|
813
|
+
}
|
|
808
814
|
|
|
809
815
|
const friendlyChanges = result.changed.map((field) => {
|
|
810
816
|
if (field === 'stateId') return 'state';
|
package/src/shared.js
CHANGED
|
@@ -18,7 +18,7 @@ export function isPiCodingAgentRoot(dir) {
|
|
|
18
18
|
if (!fs.existsSync(pkgPath)) return false;
|
|
19
19
|
try {
|
|
20
20
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
21
|
-
return pkg?.name === '@mariozechner/pi-coding-agent';
|
|
21
|
+
return pkg?.name === '@mariozechner/pi-coding-agent' || pkg?.name === '@earendil-works/pi-coding-agent';
|
|
22
22
|
} catch {
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
@@ -50,9 +50,11 @@ export function findPiCodingAgentRoot() {
|
|
|
50
50
|
{
|
|
51
51
|
const binDir = path.dirname(entry);
|
|
52
52
|
const prefix = path.resolve(binDir, '..');
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
for (const scope of ['@mariozechner', '@earendil-works']) {
|
|
54
|
+
const candidate = path.join(prefix, 'lib', 'node_modules', scope, 'pi-coding-agent');
|
|
55
|
+
if (isPiCodingAgentRoot(candidate)) {
|
|
56
|
+
return candidate;
|
|
57
|
+
}
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -60,6 +62,9 @@ export function findPiCodingAgentRoot() {
|
|
|
60
62
|
for (const candidate of [
|
|
61
63
|
'/usr/local/lib/node_modules/@mariozechner/pi-coding-agent',
|
|
62
64
|
'/usr/lib/node_modules/@mariozechner/pi-coding-agent',
|
|
65
|
+
'/usr/local/lib/node_modules/@earendil-works/pi-coding-agent',
|
|
66
|
+
'/usr/lib/node_modules/@earendil-works/pi-coding-agent',
|
|
67
|
+
'/opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent',
|
|
63
68
|
]) {
|
|
64
69
|
if (isPiCodingAgentRoot(candidate)) {
|
|
65
70
|
return candidate;
|