mcp-intervals 1.1.0 → 1.2.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 +33 -30
- package/dist/cli/clients.d.ts +1 -1
- package/dist/cli/clients.js +2 -4
- package/dist/cli/init.js +133 -42
- package/dist/cli/shell.d.ts +39 -0
- package/dist/cli/shell.js +191 -0
- package/dist/client.d.ts +3 -1
- package/dist/client.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,11 +14,14 @@ npx mcp-intervals init
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
This will:
|
|
17
|
-
1. Detect
|
|
18
|
-
2.
|
|
19
|
-
3.
|
|
20
|
-
4.
|
|
21
|
-
|
|
17
|
+
1. Detect your shell (zsh, bash, or PowerShell)
|
|
18
|
+
2. Store your API token securely in your shell profile (not in config files)
|
|
19
|
+
3. Detect installed MCP clients (Claude Code, Claude Desktop, Cursor, Windsurf)
|
|
20
|
+
4. Configure the selected clients
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
|
|
24
|
+
The installer stores your API token as an environment variable in your shell profile (`~/.zshrc`, `~/.bashrc`, or PowerShell profile), keeping it out of project files that might be committed to git. MCP config files only contain the command reference, not the token.
|
|
22
25
|
|
|
23
26
|
## Manual Setup
|
|
24
27
|
|
|
@@ -28,37 +31,43 @@ This will:
|
|
|
28
31
|
2. Go to **Options** (bottom-left) > **My Account** > **API Access**
|
|
29
32
|
3. Copy your **API token**
|
|
30
33
|
|
|
31
|
-
### 2.
|
|
34
|
+
### 2. Add the token to your shell profile
|
|
32
35
|
|
|
36
|
+
Add the following line to your shell profile:
|
|
37
|
+
|
|
38
|
+
**macOS/Linux (zsh)** - Add to `~/.zshrc`:
|
|
33
39
|
```bash
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
export INTERVALS_API_TOKEN="YOUR_TOKEN"
|
|
41
|
+
```
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
**macOS/Linux (bash)** - Add to `~/.bashrc` or `~/.bash_profile`:
|
|
44
|
+
```bash
|
|
45
|
+
export INTERVALS_API_TOKEN="YOUR_TOKEN"
|
|
46
|
+
```
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
**Windows (PowerShell)** - Add to your PowerShell profile:
|
|
49
|
+
```powershell
|
|
50
|
+
$env:INTERVALS_API_TOKEN = "YOUR_TOKEN"
|
|
42
51
|
```
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
Then reload your shell or restart your terminal.
|
|
45
54
|
|
|
46
|
-
### 3.
|
|
55
|
+
### 3. Configure MCP clients
|
|
47
56
|
|
|
48
|
-
Add this to your config file:
|
|
57
|
+
Add this to your MCP config file (token is read from environment):
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
**Claude Code:**
|
|
60
|
+
```bash
|
|
61
|
+
claude mcp add intervals --scope user -- npx -y mcp-intervals
|
|
62
|
+
```
|
|
52
63
|
|
|
64
|
+
**Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
53
65
|
```json
|
|
54
66
|
{
|
|
55
67
|
"mcpServers": {
|
|
56
68
|
"intervals": {
|
|
57
69
|
"command": "npx",
|
|
58
|
-
"args": ["-y", "mcp-intervals"]
|
|
59
|
-
"env": {
|
|
60
|
-
"INTERVALS_API_TOKEN": "YOUR_TOKEN"
|
|
61
|
-
}
|
|
70
|
+
"args": ["-y", "mcp-intervals"]
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
}
|
|
@@ -67,17 +76,14 @@ Add this to your config file:
|
|
|
67
76
|
<details>
|
|
68
77
|
<summary><strong>Cursor</strong></summary>
|
|
69
78
|
|
|
70
|
-
Add to
|
|
79
|
+
Add to `~/.cursor/mcp.json`:
|
|
71
80
|
|
|
72
81
|
```json
|
|
73
82
|
{
|
|
74
83
|
"mcpServers": {
|
|
75
84
|
"intervals": {
|
|
76
85
|
"command": "npx",
|
|
77
|
-
"args": ["-y", "mcp-intervals"]
|
|
78
|
-
"env": {
|
|
79
|
-
"INTERVALS_API_TOKEN": "YOUR_TOKEN"
|
|
80
|
-
}
|
|
86
|
+
"args": ["-y", "mcp-intervals"]
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
}
|
|
@@ -95,10 +101,7 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
95
101
|
"mcpServers": {
|
|
96
102
|
"intervals": {
|
|
97
103
|
"command": "npx",
|
|
98
|
-
"args": ["-y", "mcp-intervals"]
|
|
99
|
-
"env": {
|
|
100
|
-
"INTERVALS_API_TOKEN": "YOUR_TOKEN"
|
|
101
|
-
}
|
|
104
|
+
"args": ["-y", "mcp-intervals"]
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
}
|
package/dist/cli/clients.d.ts
CHANGED
|
@@ -16,6 +16,6 @@ interface McpServerConfig {
|
|
|
16
16
|
export declare function detectClients(): McpClient[];
|
|
17
17
|
export declare function readConfig(configPath: string): McpConfig;
|
|
18
18
|
export declare function writeConfig(configPath: string, config: McpConfig): void;
|
|
19
|
-
export declare function configureClient(configPath: string
|
|
19
|
+
export declare function configureClient(configPath: string): void;
|
|
20
20
|
export declare function hasExistingConfig(configPath: string): boolean;
|
|
21
21
|
export {};
|
package/dist/cli/clients.js
CHANGED
|
@@ -111,17 +111,15 @@ export function writeConfig(configPath, config) {
|
|
|
111
111
|
}
|
|
112
112
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
113
113
|
}
|
|
114
|
-
export function configureClient(configPath
|
|
114
|
+
export function configureClient(configPath) {
|
|
115
115
|
const config = readConfig(configPath);
|
|
116
116
|
if (!config.mcpServers) {
|
|
117
117
|
config.mcpServers = {};
|
|
118
118
|
}
|
|
119
|
+
// Don't include token in config - it's stored in shell profile
|
|
119
120
|
config.mcpServers.intervals = {
|
|
120
121
|
command: "npx",
|
|
121
122
|
args: ["-y", "mcp-intervals"],
|
|
122
|
-
env: {
|
|
123
|
-
INTERVALS_API_TOKEN: token,
|
|
124
|
-
},
|
|
125
123
|
};
|
|
126
124
|
writeConfig(configPath, config);
|
|
127
125
|
}
|
package/dist/cli/init.js
CHANGED
|
@@ -4,6 +4,7 @@ import pc from "picocolors";
|
|
|
4
4
|
import { detectClients, configureClient, hasExistingConfig } from "./clients.js";
|
|
5
5
|
import { validateToken } from "./api.js";
|
|
6
6
|
import { searchMultiselect, cancelSymbol } from "./prompts/search-multiselect.js";
|
|
7
|
+
import { detectShell, getProfilePath, findExistingToken, saveTokenToProfile, getReloadCommand, getManualInstruction, } from "./shell.js";
|
|
7
8
|
// Logo ASCII art with gradient grays (256-color)
|
|
8
9
|
const LOGO_LINES = [
|
|
9
10
|
"██╗███╗ ██╗████████╗███████╗██████╗ ██╗ ██╗ █████╗ ██╗ ███████╗",
|
|
@@ -36,12 +37,107 @@ function shortenPath(fullPath) {
|
|
|
36
37
|
}
|
|
37
38
|
return fullPath;
|
|
38
39
|
}
|
|
40
|
+
function maskToken(token) {
|
|
41
|
+
if (token.length <= 6) {
|
|
42
|
+
return "***";
|
|
43
|
+
}
|
|
44
|
+
const start = token.slice(0, 4);
|
|
45
|
+
const end = token.slice(-3);
|
|
46
|
+
return `${start}***${end}`;
|
|
47
|
+
}
|
|
39
48
|
async function main() {
|
|
40
49
|
showLogo();
|
|
41
50
|
console.log();
|
|
42
51
|
p.intro(pc.bgCyan(pc.black(" intervals ")));
|
|
43
52
|
const spinner = p.spinner();
|
|
44
|
-
// Detect
|
|
53
|
+
// Step 1: Detect shell
|
|
54
|
+
spinner.start("Detecting shell...");
|
|
55
|
+
let shellInfo = detectShell();
|
|
56
|
+
spinner.stop(shellInfo.detected
|
|
57
|
+
? `Detected ${shellInfo.type} shell`
|
|
58
|
+
: `Could not detect shell, assuming ${shellInfo.type}`);
|
|
59
|
+
// If shell not detected, ask user
|
|
60
|
+
if (!shellInfo.detected) {
|
|
61
|
+
console.log();
|
|
62
|
+
const selectedShell = await p.select({
|
|
63
|
+
message: "Which shell do you use?",
|
|
64
|
+
options: [
|
|
65
|
+
{ value: "zsh", label: "zsh" },
|
|
66
|
+
{ value: "bash", label: "bash" },
|
|
67
|
+
{ value: "powershell", label: "PowerShell" },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
if (p.isCancel(selectedShell)) {
|
|
71
|
+
p.cancel("Installation cancelled");
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
shellInfo = {
|
|
75
|
+
type: selectedShell,
|
|
76
|
+
profilePath: getProfilePath(selectedShell),
|
|
77
|
+
detected: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Step 2: Check for existing token
|
|
81
|
+
const existingToken = findExistingToken(shellInfo.profilePath, shellInfo.type);
|
|
82
|
+
let token;
|
|
83
|
+
if (existingToken.found && existingToken.value) {
|
|
84
|
+
console.log();
|
|
85
|
+
p.log.info(`Found existing token: ${pc.cyan(existingToken.partial)}`);
|
|
86
|
+
p.log.message(pc.dim(` in ${shortenPath(shellInfo.profilePath)}`));
|
|
87
|
+
const replaceToken = await p.select({
|
|
88
|
+
message: "What do you want to do?",
|
|
89
|
+
options: [
|
|
90
|
+
{ value: "keep", label: "Keep existing token" },
|
|
91
|
+
{ value: "replace", label: "Replace with new token" },
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
if (p.isCancel(replaceToken)) {
|
|
95
|
+
p.cancel("Installation cancelled");
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
if (replaceToken === "keep") {
|
|
99
|
+
token = existingToken.value;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.log();
|
|
103
|
+
token = await p.password({
|
|
104
|
+
message: "Enter your new Intervals API token:",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// No existing token, ask for one
|
|
110
|
+
console.log();
|
|
111
|
+
token = await p.password({
|
|
112
|
+
message: "Enter your Intervals API token:",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (p.isCancel(token) || !token || token.trim() === "") {
|
|
116
|
+
p.cancel("No token provided");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const finalToken = token.trim();
|
|
120
|
+
// Step 3: Validate token
|
|
121
|
+
spinner.start("Validating token...");
|
|
122
|
+
const validation = await validateToken(finalToken);
|
|
123
|
+
if (!validation.valid) {
|
|
124
|
+
spinner.stop(pc.red("Token validation failed"));
|
|
125
|
+
p.log.error(validation.error || "Invalid token");
|
|
126
|
+
p.log.message(pc.dim(" Find your API token at: https://[subdomain].myintervals.com/account/api/"));
|
|
127
|
+
console.log();
|
|
128
|
+
const continueAnyway = await p.confirm({
|
|
129
|
+
message: "Save configuration anyway (without validation)?",
|
|
130
|
+
initialValue: false,
|
|
131
|
+
});
|
|
132
|
+
if (p.isCancel(continueAnyway) || !continueAnyway) {
|
|
133
|
+
p.cancel("Installation cancelled");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
spinner.stop(`Token valid! Connected to "${validation.workspace}"`);
|
|
139
|
+
}
|
|
140
|
+
// Step 4: Detect MCP clients
|
|
45
141
|
spinner.start("Detecting MCP clients...");
|
|
46
142
|
const clients = detectClients();
|
|
47
143
|
const detectedClients = clients.filter((c) => c.detected);
|
|
@@ -60,13 +156,13 @@ async function main() {
|
|
|
60
156
|
p.cancel("No MCP clients detected. Install Claude Code, Claude Desktop, Cursor, or Windsurf first.");
|
|
61
157
|
process.exit(1);
|
|
62
158
|
}
|
|
63
|
-
// Select clients
|
|
159
|
+
// Step 5: Select clients
|
|
64
160
|
const clientChoices = detectedClients.map((client) => {
|
|
65
161
|
const hasExisting = hasExistingConfig(client.configPath);
|
|
66
162
|
return {
|
|
67
163
|
value: client.id,
|
|
68
164
|
label: client.name,
|
|
69
|
-
hint: shortenPath(client.configPath) + (hasExisting ? " - will
|
|
165
|
+
hint: shortenPath(client.configPath) + (hasExisting ? " - will update" : ""),
|
|
70
166
|
};
|
|
71
167
|
});
|
|
72
168
|
// Pre-select clients that don't have existing config
|
|
@@ -85,41 +181,14 @@ async function main() {
|
|
|
85
181
|
process.exit(0);
|
|
86
182
|
}
|
|
87
183
|
const selectedClients = detectedClients.filter((c) => Array.isArray(selectedIds) && selectedIds.includes(c.id));
|
|
88
|
-
//
|
|
184
|
+
// Step 6: Show summary and confirm
|
|
89
185
|
console.log();
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
// Validate token
|
|
98
|
-
spinner.start("Validating token...");
|
|
99
|
-
const validation = await validateToken(token.trim());
|
|
100
|
-
if (!validation.valid) {
|
|
101
|
-
spinner.stop(pc.red("Token validation failed"));
|
|
102
|
-
p.log.error(validation.error || "Invalid token");
|
|
103
|
-
p.log.message(pc.dim(" Find your API token at: https://[subdomain].myintervals.com/account/api/"));
|
|
104
|
-
console.log();
|
|
105
|
-
const continueAnyway = await p.confirm({
|
|
106
|
-
message: "Save configuration anyway (without validation)?",
|
|
107
|
-
initialValue: false,
|
|
108
|
-
});
|
|
109
|
-
if (p.isCancel(continueAnyway) || !continueAnyway) {
|
|
110
|
-
p.cancel("Installation cancelled");
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
spinner.stop(`Token valid! Connected to "${validation.workspace}"`);
|
|
116
|
-
}
|
|
117
|
-
// Show summary and confirm
|
|
118
|
-
console.log();
|
|
119
|
-
const summaryLines = [];
|
|
120
|
-
for (const client of selectedClients) {
|
|
121
|
-
summaryLines.push(`${pc.cyan(client.name)} ${pc.dim(shortenPath(client.configPath))}`);
|
|
122
|
-
}
|
|
186
|
+
const summaryLines = [
|
|
187
|
+
`${pc.bold("Token:")} ${shortenPath(shellInfo.profilePath)}`,
|
|
188
|
+
"",
|
|
189
|
+
pc.bold("MCP clients:"),
|
|
190
|
+
...selectedClients.map((c) => ` ${c.name} ${pc.dim(shortenPath(c.configPath))}`),
|
|
191
|
+
];
|
|
123
192
|
p.note(summaryLines.join("\n"), "Will configure");
|
|
124
193
|
const confirmed = await p.confirm({
|
|
125
194
|
message: "Proceed with configuration?",
|
|
@@ -129,26 +198,42 @@ async function main() {
|
|
|
129
198
|
p.cancel("Installation cancelled");
|
|
130
199
|
process.exit(0);
|
|
131
200
|
}
|
|
132
|
-
//
|
|
133
|
-
spinner.start("Saving
|
|
201
|
+
// Step 7: Save token to shell profile
|
|
202
|
+
spinner.start("Saving token to shell profile...");
|
|
203
|
+
const tokenResult = saveTokenToProfile(shellInfo.profilePath, shellInfo.type, finalToken);
|
|
204
|
+
if (!tokenResult.success) {
|
|
205
|
+
spinner.stop(pc.red("Failed to save token"));
|
|
206
|
+
console.log();
|
|
207
|
+
p.log.error(`Cannot write to ${shortenPath(shellInfo.profilePath)}`);
|
|
208
|
+
p.log.message("");
|
|
209
|
+
p.log.message(pc.bold("Add this line manually to your shell profile:"));
|
|
210
|
+
p.log.message("");
|
|
211
|
+
p.log.message(` ${pc.cyan(getManualInstruction(finalToken, shellInfo.type))}`);
|
|
212
|
+
console.log();
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
spinner.stop(`Token saved to ${shortenPath(shellInfo.profilePath)}`);
|
|
216
|
+
}
|
|
217
|
+
// Step 8: Configure MCP clients
|
|
218
|
+
spinner.start("Configuring MCP clients...");
|
|
134
219
|
const results = [];
|
|
135
220
|
for (const client of selectedClients) {
|
|
136
221
|
try {
|
|
137
|
-
configureClient(client.configPath
|
|
222
|
+
configureClient(client.configPath);
|
|
138
223
|
results.push({ client: client.name, success: true });
|
|
139
224
|
}
|
|
140
225
|
catch (error) {
|
|
141
226
|
results.push({
|
|
142
227
|
client: client.name,
|
|
143
228
|
success: false,
|
|
144
|
-
error: error instanceof Error ? error.message : String(error)
|
|
229
|
+
error: error instanceof Error ? error.message : String(error),
|
|
145
230
|
});
|
|
146
231
|
}
|
|
147
232
|
}
|
|
148
233
|
const successful = results.filter((r) => r.success);
|
|
149
234
|
const failed = results.filter((r) => !r.success);
|
|
150
235
|
spinner.stop("Configuration complete");
|
|
151
|
-
// Show results
|
|
236
|
+
// Step 9: Show results
|
|
152
237
|
console.log();
|
|
153
238
|
if (successful.length > 0) {
|
|
154
239
|
const resultLines = successful.map((r) => {
|
|
@@ -164,6 +249,12 @@ async function main() {
|
|
|
164
249
|
p.log.message(` ${pc.red("✗")} ${r.client}: ${pc.dim(r.error)}`);
|
|
165
250
|
}
|
|
166
251
|
}
|
|
252
|
+
// Step 10: Show reload instructions
|
|
253
|
+
if (tokenResult.success) {
|
|
254
|
+
console.log();
|
|
255
|
+
const reloadCmd = getReloadCommand(shellInfo.profilePath, shellInfo.type);
|
|
256
|
+
p.note(`${pc.bold("To activate the token, run:")}\n\n ${pc.cyan(reloadCmd)}\n\nOr restart your terminal.`, "Next step");
|
|
257
|
+
}
|
|
167
258
|
console.log();
|
|
168
259
|
p.outro(pc.green("Done! Restart your MCP clients to use mcp-intervals."));
|
|
169
260
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type ShellType = "zsh" | "bash" | "powershell";
|
|
2
|
+
export interface ShellInfo {
|
|
3
|
+
type: ShellType;
|
|
4
|
+
profilePath: string;
|
|
5
|
+
detected: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ExistingToken {
|
|
8
|
+
found: boolean;
|
|
9
|
+
value?: string;
|
|
10
|
+
partial?: string;
|
|
11
|
+
lineNumber?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Detect the user's shell and return profile path
|
|
15
|
+
*/
|
|
16
|
+
export declare function detectShell(): ShellInfo;
|
|
17
|
+
/**
|
|
18
|
+
* Get profile path for a specific shell type
|
|
19
|
+
*/
|
|
20
|
+
export declare function getProfilePath(shellType: ShellType): string;
|
|
21
|
+
/**
|
|
22
|
+
* Check if token already exists in shell profile
|
|
23
|
+
*/
|
|
24
|
+
export declare function findExistingToken(profilePath: string, shellType: ShellType): ExistingToken;
|
|
25
|
+
/**
|
|
26
|
+
* Save token to shell profile
|
|
27
|
+
*/
|
|
28
|
+
export declare function saveTokenToProfile(profilePath: string, shellType: ShellType, token: string): {
|
|
29
|
+
success: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Get the command to reload the shell profile
|
|
34
|
+
*/
|
|
35
|
+
export declare function getReloadCommand(profilePath: string, shellType: ShellType): string;
|
|
36
|
+
/**
|
|
37
|
+
* Get manual instruction line for when auto-save fails
|
|
38
|
+
*/
|
|
39
|
+
export declare function getManualInstruction(token: string, shellType: ShellType): string;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
const TOKEN_VAR_NAME = "INTERVALS_API_TOKEN";
|
|
5
|
+
/**
|
|
6
|
+
* Detect the user's shell and return profile path
|
|
7
|
+
*/
|
|
8
|
+
export function detectShell() {
|
|
9
|
+
const platform = os.platform();
|
|
10
|
+
const home = os.homedir();
|
|
11
|
+
if (platform === "win32") {
|
|
12
|
+
// Windows: assume PowerShell
|
|
13
|
+
const profilePath = process.env.USERPROFILE
|
|
14
|
+
? path.join(process.env.USERPROFILE, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1")
|
|
15
|
+
: path.join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
16
|
+
return {
|
|
17
|
+
type: "powershell",
|
|
18
|
+
profilePath,
|
|
19
|
+
detected: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// macOS/Linux: check $SHELL
|
|
23
|
+
const shellEnv = process.env.SHELL || "";
|
|
24
|
+
if (shellEnv.endsWith("zsh")) {
|
|
25
|
+
return {
|
|
26
|
+
type: "zsh",
|
|
27
|
+
profilePath: path.join(home, ".zshrc"),
|
|
28
|
+
detected: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (shellEnv.endsWith("bash")) {
|
|
32
|
+
// On macOS, prefer .bash_profile for login shells
|
|
33
|
+
const bashProfile = path.join(home, ".bash_profile");
|
|
34
|
+
const bashrc = path.join(home, ".bashrc");
|
|
35
|
+
if (platform === "darwin" && fs.existsSync(bashProfile)) {
|
|
36
|
+
return {
|
|
37
|
+
type: "bash",
|
|
38
|
+
profilePath: bashProfile,
|
|
39
|
+
detected: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
type: "bash",
|
|
44
|
+
profilePath: bashrc,
|
|
45
|
+
detected: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Default to zsh on macOS, bash on Linux
|
|
49
|
+
if (platform === "darwin") {
|
|
50
|
+
return {
|
|
51
|
+
type: "zsh",
|
|
52
|
+
profilePath: path.join(home, ".zshrc"),
|
|
53
|
+
detected: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
type: "bash",
|
|
58
|
+
profilePath: path.join(home, ".bashrc"),
|
|
59
|
+
detected: false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get profile path for a specific shell type
|
|
64
|
+
*/
|
|
65
|
+
export function getProfilePath(shellType) {
|
|
66
|
+
const platform = os.platform();
|
|
67
|
+
const home = os.homedir();
|
|
68
|
+
switch (shellType) {
|
|
69
|
+
case "zsh":
|
|
70
|
+
return path.join(home, ".zshrc");
|
|
71
|
+
case "bash":
|
|
72
|
+
if (platform === "darwin") {
|
|
73
|
+
const bashProfile = path.join(home, ".bash_profile");
|
|
74
|
+
if (fs.existsSync(bashProfile)) {
|
|
75
|
+
return bashProfile;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return path.join(home, ".bashrc");
|
|
79
|
+
case "powershell":
|
|
80
|
+
return process.env.USERPROFILE
|
|
81
|
+
? path.join(process.env.USERPROFILE, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1")
|
|
82
|
+
: path.join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create partial token for display (e.g., "7ni6***chj")
|
|
87
|
+
*/
|
|
88
|
+
function maskToken(token) {
|
|
89
|
+
if (token.length <= 6) {
|
|
90
|
+
return "***";
|
|
91
|
+
}
|
|
92
|
+
const start = token.slice(0, 4);
|
|
93
|
+
const end = token.slice(-3);
|
|
94
|
+
return `${start}***${end}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check if token already exists in shell profile
|
|
98
|
+
*/
|
|
99
|
+
export function findExistingToken(profilePath, shellType) {
|
|
100
|
+
if (!fs.existsSync(profilePath)) {
|
|
101
|
+
return { found: false };
|
|
102
|
+
}
|
|
103
|
+
const content = fs.readFileSync(profilePath, "utf-8");
|
|
104
|
+
const lines = content.split("\n");
|
|
105
|
+
// Pattern depends on shell type
|
|
106
|
+
const pattern = shellType === "powershell"
|
|
107
|
+
? /\$env:INTERVALS_API_TOKEN\s*=\s*["']([^"']+)["']/
|
|
108
|
+
: /export\s+INTERVALS_API_TOKEN\s*=\s*["']?([^"'\s]+)["']?/;
|
|
109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
110
|
+
const match = lines[i].match(pattern);
|
|
111
|
+
if (match) {
|
|
112
|
+
const value = match[1];
|
|
113
|
+
return {
|
|
114
|
+
found: true,
|
|
115
|
+
value,
|
|
116
|
+
partial: maskToken(value),
|
|
117
|
+
lineNumber: i + 1,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { found: false };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Format the export line for the given shell
|
|
125
|
+
*/
|
|
126
|
+
function formatExportLine(token, shellType) {
|
|
127
|
+
if (shellType === "powershell") {
|
|
128
|
+
return `$env:${TOKEN_VAR_NAME} = "${token}"`;
|
|
129
|
+
}
|
|
130
|
+
return `export ${TOKEN_VAR_NAME}="${token}"`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Save token to shell profile
|
|
134
|
+
*/
|
|
135
|
+
export function saveTokenToProfile(profilePath, shellType, token) {
|
|
136
|
+
try {
|
|
137
|
+
const dir = path.dirname(profilePath);
|
|
138
|
+
// Create directory if it doesn't exist (for PowerShell profile)
|
|
139
|
+
if (!fs.existsSync(dir)) {
|
|
140
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
141
|
+
}
|
|
142
|
+
const exportLine = formatExportLine(token, shellType);
|
|
143
|
+
const existingToken = findExistingToken(profilePath, shellType);
|
|
144
|
+
if (!fs.existsSync(profilePath)) {
|
|
145
|
+
// Create new file
|
|
146
|
+
fs.writeFileSync(profilePath, exportLine + "\n", { mode: 0o644 });
|
|
147
|
+
return { success: true };
|
|
148
|
+
}
|
|
149
|
+
let content = fs.readFileSync(profilePath, "utf-8");
|
|
150
|
+
if (existingToken.found) {
|
|
151
|
+
// Replace existing line
|
|
152
|
+
const pattern = shellType === "powershell"
|
|
153
|
+
? /\$env:INTERVALS_API_TOKEN\s*=\s*["'][^"']*["']/g
|
|
154
|
+
: /export\s+INTERVALS_API_TOKEN\s*=\s*["']?[^"'\s]*["']?/g;
|
|
155
|
+
content = content.replace(pattern, exportLine);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Append to file
|
|
159
|
+
const newline = content.endsWith("\n") ? "" : "\n";
|
|
160
|
+
content = content + newline + "\n# Intervals API token\n" + exportLine + "\n";
|
|
161
|
+
}
|
|
162
|
+
fs.writeFileSync(profilePath, content, { mode: 0o644 });
|
|
163
|
+
return { success: true };
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: error instanceof Error ? error.message : String(error),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get the command to reload the shell profile
|
|
174
|
+
*/
|
|
175
|
+
export function getReloadCommand(profilePath, shellType) {
|
|
176
|
+
if (shellType === "powershell") {
|
|
177
|
+
return ". $PROFILE";
|
|
178
|
+
}
|
|
179
|
+
// Use ~ notation for display
|
|
180
|
+
const home = os.homedir();
|
|
181
|
+
const displayPath = profilePath.startsWith(home)
|
|
182
|
+
? "~" + profilePath.slice(home.length)
|
|
183
|
+
: profilePath;
|
|
184
|
+
return `source ${displayPath}`;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get manual instruction line for when auto-save fails
|
|
188
|
+
*/
|
|
189
|
+
export function getManualInstruction(token, shellType) {
|
|
190
|
+
return formatExportLine(token, shellType);
|
|
191
|
+
}
|
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -113,6 +113,7 @@ export class IntervalsClient {
|
|
|
113
113
|
// --- Me (current user) ---
|
|
114
114
|
async getMe() {
|
|
115
115
|
const data = await this.request(`/me/`);
|
|
116
|
-
|
|
116
|
+
// Return the first user object with personid from the top-level response
|
|
117
|
+
return { ...data.me[0], personid: data.personid };
|
|
117
118
|
}
|
|
118
119
|
}
|