@alasano/pi-linear 0.3.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/README.md CHANGED
@@ -34,27 +34,34 @@ pi install npm:@alasano/pi-linear
34
34
 
35
35
  ## Authentication
36
36
 
37
- The extension auto-detects the `LINEAR_API_KEY` environment variable if set globally. No setup needed in that case.
37
+ The extension can use either stored workspaces or the `LINEAR_API_KEY` environment variable. Stored workspaces are preferred by default so multi-account switching keeps working even if `LINEAR_API_KEY` is set globally.
38
38
 
39
- For multi-workspace support or if you don't have the env var, use the `/linear-auth` command:
39
+ Use the `/linear-auth` command to manage workspaces and choose the auth preference:
40
40
 
41
- | Command | Description |
42
- | ---------------------------- | -------------------------------------------------------- |
43
- | `/linear-auth add <name>` | Add a workspace with an API key (prompts for key) |
44
- | `/linear-auth remove <name>` | Remove a workspace (shows selector if name omitted) |
45
- | `/linear-auth switch <name>` | Switch active workspace (shows selector if name omitted) |
46
- | `/linear-auth status` | Show current auth source and configured workspaces |
47
- | `/linear-auth` | Same as status |
41
+ | Command | Description |
42
+ | ------------------------------------ | -------------------------------------------------------------------- |
43
+ | `/linear-auth add <name>` | Add a workspace with an API key (prompts for key) |
44
+ | `/linear-auth remove <name>` | Remove a workspace (shows selector if name omitted) |
45
+ | `/linear-auth switch <name>` | Switch active workspace and prefer stored workspaces |
46
+ | `/linear-auth prefer workspace\|env` | Choose whether stored workspaces or `LINEAR_API_KEY` are tried first |
47
+ | `/linear-auth status` | Show auth preference, active source, and configured workspaces |
48
+ | `/linear-auth` | Same as status |
48
49
 
49
50
  When you add your first workspace, it becomes active automatically. Adding a second workspace prompts you to switch. After a `/reload` with two or more workspaces configured, the agent gets a `linear_switch_workspace` tool and can switch between them when asked.
50
51
 
51
- Auth resolution order:
52
+ Default auth resolution order (`/linear-auth prefer workspace`):
52
53
 
53
54
  1. Active stored workspace (if any workspaces are configured)
54
- 2. `LINEAR_API_KEY` environment variable (fallback when no workspaces configured)
55
+ 2. `LINEAR_API_KEY` environment variable (fallback when no workspace is active)
55
56
  3. Interactive prompt (asks you to set up a key)
56
57
 
57
- Credentials are stored at `~/.pi/agent/state/extensions/linear/credentials.json`.
58
+ Environment auth resolution order (`/linear-auth prefer env`):
59
+
60
+ 1. `LINEAR_API_KEY` environment variable
61
+ 2. Active stored workspace (fallback when `LINEAR_API_KEY` is not set)
62
+ 3. Interactive prompt (asks you to set up a key)
63
+
64
+ Credentials and auth preference are stored at `~/.pi/agent/extensions/linear/credentials.json`.
58
65
 
59
66
  ## Tool settings
60
67
 
@@ -9,13 +9,20 @@ import { ISSUE_SELECTION } from './selections';
9
9
  const LINEAR_GRAPHQL_ENDPOINT = 'https://api.linear.app/graphql';
10
10
  const IDENTIFIER_PATTERN = /^([A-Z][A-Z0-9]*)-(\d+)$/i;
11
11
 
12
+ export type AuthPreference = 'workspace' | 'env';
13
+
12
14
  export type WorkspaceCredentials = {
13
15
  activeWorkspace: string | null;
16
+ authPreference: AuthPreference;
14
17
  workspaces: Record<string, { apiKey: string }>;
15
18
  };
16
19
 
20
+ function normalizeAuthPreference(value: unknown): AuthPreference {
21
+ return value === 'env' ? 'env' : 'workspace';
22
+ }
23
+
17
24
  function emptyCredentials(): WorkspaceCredentials {
18
- return { activeWorkspace: null, workspaces: {} };
25
+ return { activeWorkspace: null, authPreference: 'workspace', workspaces: {} };
19
26
  }
20
27
 
21
28
  export function getCredentialFilePath() {
@@ -38,6 +45,7 @@ export async function readCredentials(): Promise<WorkspaceCredentials> {
38
45
  }
39
46
  const activeWorkspace =
40
47
  typeof parsed.activeWorkspace === 'string' ? parsed.activeWorkspace : null;
48
+ const authPreference = normalizeAuthPreference(parsed.authPreference);
41
49
  const workspaces: Record<string, { apiKey: string }> = {};
42
50
  for (const [name, entry] of Object.entries(parsed.workspaces)) {
43
51
  if (
@@ -49,7 +57,7 @@ export async function readCredentials(): Promise<WorkspaceCredentials> {
49
57
  workspaces[name] = { apiKey: (entry as { apiKey: string }).apiKey };
50
58
  }
51
59
  }
52
- return { activeWorkspace, workspaces };
60
+ return { activeWorkspace, authPreference, workspaces };
53
61
  } catch {
54
62
  return emptyCredentials();
55
63
  }
@@ -96,6 +104,14 @@ export async function switchWorkspace(name: string): Promise<WorkspaceCredential
96
104
  throw new Error(`Workspace "${name}" does not exist.`);
97
105
  }
98
106
  creds.activeWorkspace = name;
107
+ creds.authPreference = 'workspace';
108
+ await writeCredentials(creds);
109
+ return creds;
110
+ }
111
+
112
+ export async function setAuthPreference(preference: AuthPreference): Promise<WorkspaceCredentials> {
113
+ const creds = await readCredentials();
114
+ creds.authPreference = preference;
99
115
  await writeCredentials(creds);
100
116
  return creds;
101
117
  }
@@ -114,10 +130,15 @@ export async function resolveApiKey(
114
130
  ): Promise<{ apiKey?: string; source: 'env' | 'workspace' | 'none' }> {
115
131
  const creds = await readCredentials();
116
132
  const workspaceKey = getActiveApiKey(creds);
117
- if (workspaceKey) return { apiKey: workspaceKey, source: 'workspace' };
118
-
119
133
  const envApiKey = asString(process.env.LINEAR_API_KEY);
120
- if (envApiKey) return { apiKey: envApiKey, source: 'env' };
134
+
135
+ if (creds.authPreference === 'env') {
136
+ if (envApiKey) return { apiKey: envApiKey, source: 'env' };
137
+ if (workspaceKey) return { apiKey: workspaceKey, source: 'workspace' };
138
+ } else {
139
+ if (workspaceKey) return { apiKey: workspaceKey, source: 'workspace' };
140
+ if (envApiKey) return { apiKey: envApiKey, source: 'env' };
141
+ }
121
142
 
122
143
  const promptIfMissing = options?.promptIfMissing ?? true;
123
144
  if (promptIfMissing && ctx.hasUI) {
@@ -4,6 +4,7 @@ import {
4
4
  addWorkspace,
5
5
  removeWorkspace,
6
6
  switchWorkspace,
7
+ setAuthPreference,
7
8
  listWorkspaceNames,
8
9
  getActiveWorkspaceName,
9
10
  resolveApiKey,
@@ -27,7 +28,8 @@ import { registerLinearSettings } from './settings';
27
28
 
28
29
  export default async function linearExtension(pi: ExtensionAPI) {
29
30
  pi.registerCommand('linear-auth', {
30
- description: 'Manage Linear workspace auth (usage: /linear-auth [add|remove|switch|status])',
31
+ description:
32
+ 'Manage Linear workspace auth (usage: /linear-auth [add|remove|switch|prefer|status])',
31
33
  handler: async (args, ctx) => {
32
34
  const parts = args.trim().split(/\s+/);
33
35
  const subcommand = parts[0]?.toLowerCase() || '';
@@ -145,7 +147,37 @@ export default async function linearExtension(pi: ExtensionAPI) {
145
147
  }
146
148
 
147
149
  await switchWorkspace(workspaceName);
148
- ctx.ui.notify(`Active workspace: ${workspaceName}`, 'info');
150
+ ctx.ui.notify(`Active workspace: ${workspaceName}\nAuth preference: workspace`, 'info');
151
+ return;
152
+ }
153
+
154
+ case 'prefer': {
155
+ const preference = name.toLowerCase();
156
+ if (preference !== 'workspace' && preference !== 'env') {
157
+ ctx.ui.notify('Usage: /linear-auth prefer [workspace|env]', 'warning');
158
+ return;
159
+ }
160
+
161
+ const updated = await setAuthPreference(preference);
162
+ const envIsSet = Boolean(asString(process.env.LINEAR_API_KEY));
163
+ const active = getActiveWorkspaceName(updated);
164
+ const lines = [`Auth preference: ${preference}`];
165
+
166
+ if (preference === 'env') {
167
+ if (envIsSet) {
168
+ lines.push('LINEAR_API_KEY will be used before stored workspaces.');
169
+ } else if (active) {
170
+ lines.push('LINEAR_API_KEY is not set; falling back to the active workspace.');
171
+ } else {
172
+ lines.push('LINEAR_API_KEY is not set and no active workspace is configured.');
173
+ }
174
+ } else if (active) {
175
+ lines.push(`Workspace "${active}" will be used before LINEAR_API_KEY.`);
176
+ } else {
177
+ lines.push('No active workspace configured; falling back to LINEAR_API_KEY if set.');
178
+ }
179
+
180
+ ctx.ui.notify(lines.join('\n'), preference === 'env' && !envIsSet ? 'warning' : 'info');
149
181
  return;
150
182
  }
151
183
 
@@ -157,6 +189,7 @@ export default async function linearExtension(pi: ExtensionAPI) {
157
189
  });
158
190
  const names = listWorkspaceNames(creds);
159
191
  const active = getActiveWorkspaceName(creds);
192
+ const envIsSet = Boolean(asString(process.env.LINEAR_API_KEY));
160
193
 
161
194
  let sourceLabel: string;
162
195
  if (source === 'workspace') {
@@ -167,7 +200,7 @@ export default async function linearExtension(pi: ExtensionAPI) {
167
200
  sourceLabel = 'none';
168
201
  }
169
202
 
170
- const lines = [`Auth source: ${sourceLabel}`];
203
+ const lines = [`Auth preference: ${creds.authPreference}`, `Auth source: ${sourceLabel}`];
171
204
  if (names.length > 0) {
172
205
  lines.push(
173
206
  `Workspaces: ${names.map((n) => (n === active ? `${n} (active)` : n)).join(', ')}`,
@@ -176,12 +209,34 @@ export default async function linearExtension(pi: ExtensionAPI) {
176
209
  lines.push('No workspaces configured');
177
210
  }
178
211
 
212
+ if (creds.authPreference === 'workspace' && envIsSet) {
213
+ if (source === 'workspace') {
214
+ lines.push(
215
+ 'LINEAR_API_KEY is set but not active. Run /linear-auth prefer env to use it.',
216
+ );
217
+ } else if (source === 'env') {
218
+ lines.push(
219
+ 'No active workspace is configured, so LINEAR_API_KEY is being used as fallback.',
220
+ );
221
+ }
222
+ } else if (creds.authPreference === 'env' && names.length > 0) {
223
+ if (source === 'env') {
224
+ lines.push(
225
+ 'Stored workspaces are available but not active. Run /linear-auth prefer workspace or /linear-auth switch <name> to use one.',
226
+ );
227
+ } else if (source === 'workspace') {
228
+ lines.push(
229
+ 'LINEAR_API_KEY is not set, so the active workspace is being used as fallback.',
230
+ );
231
+ }
232
+ }
233
+
179
234
  ctx.ui.notify(lines.join('\n'), source === 'none' ? 'warning' : 'info');
180
235
  return;
181
236
  }
182
237
 
183
238
  default: {
184
- ctx.ui.notify('Usage: /linear-auth [add|remove|switch|status]', 'warning');
239
+ ctx.ui.notify('Usage: /linear-auth [add|remove|switch|prefer|status]', 'warning');
185
240
  }
186
241
  }
187
242
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alasano/pi-linear",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Linear integration for pi with 55+ tools, multi-workspace auth, and per-tool settings",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "type": "module",
15
15
  "scripts": {
16
+ "test": "vitest run",
16
17
  "typecheck": "tsc --noEmit"
17
18
  },
18
19
  "pi": {