@fink-andreas/pi-linear-tools 0.2.1 → 0.4.0
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 +42 -0
- package/extensions/pi-linear-tools.js +167 -134
- package/index.js +167 -134
- package/package.json +2 -2
- package/src/auth/callback-server.js +1 -1
- package/src/auth/constants.js +9 -0
- package/src/auth/index.js +50 -7
- package/src/auth/oauth.js +6 -5
- package/src/auth/pkce.js +16 -53
- package/src/auth/token-refresh.js +5 -7
- package/src/cli.js +1 -13
- package/src/error-hints.js +24 -0
- package/src/handlers.js +50 -45
- package/src/linear-client.js +252 -11
- package/src/linear.js +1066 -636
- package/src/logger.js +56 -16
- package/src/settings.js +5 -4
- package/src/shared.js +112 -0
package/src/logger.js
CHANGED
|
@@ -1,11 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structured logging module
|
|
2
|
+
* Structured logging module (file-first, TUI-safe)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { mkdirSync, appendFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
|
|
5
8
|
const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
|
|
6
9
|
let currentLevel = process.env.LOG_LEVEL || 'info';
|
|
7
10
|
let quietMode = false;
|
|
8
11
|
|
|
12
|
+
const LOG_TO_CONSOLE = String(process.env.PI_LINEAR_TOOLS_LOG_TO_CONSOLE || '').toLowerCase() === 'true';
|
|
13
|
+
const DEFAULT_LOG_FILE = process.env.PI_LINEAR_TOOLS_LOG_FILE
|
|
14
|
+
|| join(process.env.HOME || process.cwd(), '.config', 'pi-linear-tools', 'pi-linear-tools.log');
|
|
15
|
+
|
|
16
|
+
let logFileReady = false;
|
|
17
|
+
let logFilePath = DEFAULT_LOG_FILE;
|
|
18
|
+
|
|
19
|
+
function ensureLogFileReady() {
|
|
20
|
+
if (logFileReady) return;
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(dirname(logFilePath), { recursive: true });
|
|
23
|
+
} catch {
|
|
24
|
+
// ignore; fallback handled in writeLogLine
|
|
25
|
+
}
|
|
26
|
+
logFileReady = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeLogLine(line, isError = false) {
|
|
30
|
+
try {
|
|
31
|
+
ensureLogFileReady();
|
|
32
|
+
appendFileSync(logFilePath, `${line}\n`, { encoding: 'utf8' });
|
|
33
|
+
} catch {
|
|
34
|
+
// Last-resort fallback is disabled by default to protect TUI.
|
|
35
|
+
// Only print when explicitly opted in.
|
|
36
|
+
if (LOG_TO_CONSOLE) {
|
|
37
|
+
if (isError) console.error(line);
|
|
38
|
+
else console.log(line);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (LOG_TO_CONSOLE) {
|
|
43
|
+
if (isError) console.error(line);
|
|
44
|
+
else console.log(line);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
9
48
|
/**
|
|
10
49
|
* Enable quiet mode (suppress info/debug/warn, keep only errors)
|
|
11
50
|
*/
|
|
@@ -35,7 +74,7 @@ function getTimestamp() {
|
|
|
35
74
|
*/
|
|
36
75
|
function maskValue(key, value) {
|
|
37
76
|
const sensitiveKeys = ['apiKey', 'token', 'password', 'secret', 'LINEAR_API_KEY'];
|
|
38
|
-
if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk.toLowerCase()))) {
|
|
77
|
+
if (sensitiveKeys.some((sk) => key.toLowerCase().includes(sk.toLowerCase()))) {
|
|
39
78
|
return '***masked***';
|
|
40
79
|
}
|
|
41
80
|
return value;
|
|
@@ -50,7 +89,7 @@ function formatLog(level, message, data = {}) {
|
|
|
50
89
|
timestamp,
|
|
51
90
|
level: level.toUpperCase(),
|
|
52
91
|
message,
|
|
53
|
-
...data
|
|
92
|
+
...data,
|
|
54
93
|
};
|
|
55
94
|
return JSON.stringify(entry);
|
|
56
95
|
}
|
|
@@ -60,7 +99,7 @@ function formatLog(level, message, data = {}) {
|
|
|
60
99
|
*/
|
|
61
100
|
export function debug(message, data = {}) {
|
|
62
101
|
if (shouldLog('debug')) {
|
|
63
|
-
|
|
102
|
+
writeLogLine(formatLog('debug', message, data));
|
|
64
103
|
}
|
|
65
104
|
}
|
|
66
105
|
|
|
@@ -69,7 +108,7 @@ export function debug(message, data = {}) {
|
|
|
69
108
|
*/
|
|
70
109
|
export function info(message, data = {}) {
|
|
71
110
|
if (shouldLog('info')) {
|
|
72
|
-
|
|
111
|
+
writeLogLine(formatLog('info', message, data));
|
|
73
112
|
}
|
|
74
113
|
}
|
|
75
114
|
|
|
@@ -78,7 +117,7 @@ export function info(message, data = {}) {
|
|
|
78
117
|
*/
|
|
79
118
|
export function warn(message, data = {}) {
|
|
80
119
|
if (shouldLog('warn')) {
|
|
81
|
-
|
|
120
|
+
writeLogLine(formatLog('warn', message, data));
|
|
82
121
|
}
|
|
83
122
|
}
|
|
84
123
|
|
|
@@ -87,7 +126,7 @@ export function warn(message, data = {}) {
|
|
|
87
126
|
*/
|
|
88
127
|
export function error(message, data = {}) {
|
|
89
128
|
if (shouldLog('error')) {
|
|
90
|
-
|
|
129
|
+
writeLogLine(formatLog('error', message, data), true);
|
|
91
130
|
}
|
|
92
131
|
}
|
|
93
132
|
|
|
@@ -95,13 +134,7 @@ export function error(message, data = {}) {
|
|
|
95
134
|
* Print startup banner
|
|
96
135
|
*/
|
|
97
136
|
export function printBanner() {
|
|
98
|
-
|
|
99
|
-
╔════════════════════════════════════════════════════════════╗
|
|
100
|
-
║ pi-linear-tools ║
|
|
101
|
-
║ Pi extension tools for Linear SDK workflows ║
|
|
102
|
-
╚════════════════════════════════════════════════════════════╝
|
|
103
|
-
`;
|
|
104
|
-
console.log(banner);
|
|
137
|
+
info('pi-linear-tools startup');
|
|
105
138
|
}
|
|
106
139
|
|
|
107
140
|
/**
|
|
@@ -110,8 +143,8 @@ export function printBanner() {
|
|
|
110
143
|
export function logConfig(config) {
|
|
111
144
|
info('Configuration loaded', {
|
|
112
145
|
...Object.fromEntries(
|
|
113
|
-
Object.entries(config).map(([key, value]) => [key, maskValue(key, value)])
|
|
114
|
-
)
|
|
146
|
+
Object.entries(config).map(([key, value]) => [key, maskValue(key, value)]),
|
|
147
|
+
),
|
|
115
148
|
});
|
|
116
149
|
}
|
|
117
150
|
|
|
@@ -126,3 +159,10 @@ export function setLogLevel(level) {
|
|
|
126
159
|
warn(`Invalid log level: ${level}. Using: ${currentLevel}`);
|
|
127
160
|
}
|
|
128
161
|
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Expose active log file path for diagnostics/tests
|
|
165
|
+
*/
|
|
166
|
+
export function getLogFilePath() {
|
|
167
|
+
return logFilePath;
|
|
168
|
+
}
|
package/src/settings.js
CHANGED
|
@@ -7,6 +7,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
9
|
import { debug, warn, error as logError } from './logger.js';
|
|
10
|
+
import { OAUTH_CLIENT_ID } from './auth/constants.js';
|
|
10
11
|
|
|
11
12
|
export function getDefaultSettings() {
|
|
12
13
|
return {
|
|
@@ -14,7 +15,7 @@ export function getDefaultSettings() {
|
|
|
14
15
|
authMethod: 'api-key', // 'api-key' or 'oauth'
|
|
15
16
|
apiKey: null, // Legacy API key (migrated from linearApiKey)
|
|
16
17
|
oauth: {
|
|
17
|
-
clientId:
|
|
18
|
+
clientId: OAUTH_CLIENT_ID,
|
|
18
19
|
redirectUri: 'http://localhost:34711/callback',
|
|
19
20
|
},
|
|
20
21
|
defaultTeam: null,
|
|
@@ -49,7 +50,7 @@ function migrateSettings(settings) {
|
|
|
49
50
|
// Add OAuth config
|
|
50
51
|
if (!migrated.oauth || typeof migrated.oauth !== 'object') {
|
|
51
52
|
migrated.oauth = {
|
|
52
|
-
clientId:
|
|
53
|
+
clientId: OAUTH_CLIENT_ID,
|
|
53
54
|
redirectUri: 'http://localhost:34711/callback',
|
|
54
55
|
};
|
|
55
56
|
}
|
|
@@ -73,14 +74,14 @@ function migrateSettings(settings) {
|
|
|
73
74
|
// Ensure oauth config exists
|
|
74
75
|
if (!migrated.oauth || typeof migrated.oauth !== 'object') {
|
|
75
76
|
migrated.oauth = {
|
|
76
|
-
clientId:
|
|
77
|
+
clientId: OAUTH_CLIENT_ID,
|
|
77
78
|
redirectUri: 'http://localhost:34711/callback',
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
// Ensure oauth has clientId and redirectUri
|
|
82
83
|
if (migrated.oauth.clientId === undefined) {
|
|
83
|
-
migrated.oauth.clientId =
|
|
84
|
+
migrated.oauth.clientId = OAUTH_CLIENT_ID;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
if (migrated.oauth.redirectUri === undefined) {
|
package/src/shared.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for pi-linear-tools entry points
|
|
3
|
+
*
|
|
4
|
+
* Common functions used by both CLI and extension entry points.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if a directory is a pi-coding-agent root
|
|
13
|
+
* @param {string} dir - Directory path to check
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
export function isPiCodingAgentRoot(dir) {
|
|
17
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
18
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
21
|
+
return pkg?.name === '@mariozechner/pi-coding-agent';
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find the pi-coding-agent root directory
|
|
29
|
+
* @returns {string|null}
|
|
30
|
+
*/
|
|
31
|
+
export function findPiCodingAgentRoot() {
|
|
32
|
+
const entry = process.argv?.[1];
|
|
33
|
+
if (!entry) return null;
|
|
34
|
+
|
|
35
|
+
// Method 1: walk up from argv1 (works when argv1 is .../pi-coding-agent/dist/cli.js)
|
|
36
|
+
{
|
|
37
|
+
let dir = path.dirname(entry);
|
|
38
|
+
for (let i = 0; i < 20; i += 1) {
|
|
39
|
+
if (isPiCodingAgentRoot(dir)) {
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
const parent = path.dirname(dir);
|
|
43
|
+
if (parent === dir) break;
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Method 2: npm global layout guess (works when argv1 is .../<prefix>/bin/pi)
|
|
49
|
+
// <prefix>/bin/pi -> <prefix>/lib/node_modules/@mariozechner/pi-coding-agent
|
|
50
|
+
{
|
|
51
|
+
const binDir = path.dirname(entry);
|
|
52
|
+
const prefix = path.resolve(binDir, '..');
|
|
53
|
+
const candidate = path.join(prefix, 'lib', 'node_modules', '@mariozechner', 'pi-coding-agent');
|
|
54
|
+
if (isPiCodingAgentRoot(candidate)) {
|
|
55
|
+
return candidate;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Method 3: common global node_modules locations
|
|
60
|
+
for (const candidate of [
|
|
61
|
+
'/usr/local/lib/node_modules/@mariozechner/pi-coding-agent',
|
|
62
|
+
'/usr/lib/node_modules/@mariozechner/pi-coding-agent',
|
|
63
|
+
]) {
|
|
64
|
+
if (isPiCodingAgentRoot(candidate)) {
|
|
65
|
+
return candidate;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Import from pi-coding-agent root
|
|
74
|
+
* @param {string} relativePathFromPiRoot - Path relative to pi root
|
|
75
|
+
* @returns {Promise<any>}
|
|
76
|
+
*/
|
|
77
|
+
export async function importFromPiRoot(relativePathFromPiRoot) {
|
|
78
|
+
const piRoot = findPiCodingAgentRoot();
|
|
79
|
+
|
|
80
|
+
if (!piRoot) throw new Error('Unable to locate @mariozechner/pi-coding-agent installation');
|
|
81
|
+
|
|
82
|
+
const absPath = path.join(piRoot, relativePathFromPiRoot);
|
|
83
|
+
return import(pathToFileURL(absPath).href);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse command line arguments string into tokens
|
|
88
|
+
* @param {string} argsString - Arguments string to parse
|
|
89
|
+
* @returns {string[]}
|
|
90
|
+
*/
|
|
91
|
+
export function parseArgs(argsString) {
|
|
92
|
+
if (!argsString || !argsString.trim()) return [];
|
|
93
|
+
const tokens = argsString.match(/"[^"]*"|'[^']*'|\S+/g) || [];
|
|
94
|
+
return tokens.map((t) => {
|
|
95
|
+
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
|
|
96
|
+
return t.slice(1, -1);
|
|
97
|
+
}
|
|
98
|
+
return t;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read a flag value from command line arguments
|
|
104
|
+
* @param {string[]} args - Parsed arguments
|
|
105
|
+
* @param {string} flag - Flag name
|
|
106
|
+
* @returns {string|undefined}
|
|
107
|
+
*/
|
|
108
|
+
export function readFlag(args, flag) {
|
|
109
|
+
const idx = args.indexOf(flag);
|
|
110
|
+
if (idx >= 0 && idx + 1 < args.length) return args[idx + 1];
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|