incremnt 0.1.13 → 0.1.16

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
@@ -2,13 +2,15 @@
2
2
 
3
3
  Command-line tool and MCP server for querying your [incremnt](https://incremnt.app) strength training data.
4
4
 
5
- ## Install
5
+ ## Setup
6
6
 
7
7
  ```bash
8
8
  npm install -g incremnt
9
+ incremnt login
10
+ incremnt mcp install # registers with Claude Desktop and Claude Code
9
11
  ```
10
12
 
11
- This gives you two commands: `incremnt` (CLI) and `incremnt-mcp` (MCP server).
13
+ This gives you two commands: `incremnt` (CLI) and `incremnt-mcp` (MCP server). The `mcp install` step auto-registers the MCP server — restart Claude for it to take effect.
12
14
 
13
15
  ## CLI
14
16
 
@@ -44,6 +46,8 @@ If `--input` is omitted, the CLI checks `INCREMNT_SNAPSHOT`, then `ONEMORE_SNAPS
44
46
  | `sessions show --id <id>` | Details for a single session |
45
47
  | `programs current` | Active program state |
46
48
  | `programs list` | All programs |
49
+ | `cycles list [--program-id <id>]` | Completed cycle summaries |
50
+ | `cycles show --id <id>` | Details for a completed cycle summary |
47
51
  | `exercises history --name <name>` | Set-by-set history for an exercise |
48
52
  | `records` | Personal records (best e1RM per exercise) |
49
53
  | `login` | Authenticate with the hosted sync service |
@@ -58,6 +62,7 @@ If `--input` is omitted, the CLI checks `INCREMNT_SNAPSHOT`, then `ONEMORE_SNAPS
58
62
  | `--pretty` | Human-readable formatted output (default is JSON) |
59
63
  | `--input <path>` | Path to a local `.onemore.json` snapshot |
60
64
  | `--limit <n>` | Limit number of results (for `sessions list`) |
65
+ | `--program-id <id>` | Filter cycle summaries to a program |
61
66
 
62
67
  ### Exercise matching
63
68
 
@@ -65,26 +70,23 @@ If `--input` is omitted, the CLI checks `INCREMNT_SNAPSHOT`, then `ONEMORE_SNAPS
65
70
 
66
71
  ## MCP Server
67
72
 
68
- The package includes an [MCP](https://modelcontextprotocol.io) server that exposes the same queries as tools for AI assistants like Claude.
73
+ The package includes an [MCP](https://modelcontextprotocol.io) server that exposes the same read queries and program proposal commands as tools for AI assistants like Claude.
69
74
 
70
- ### Setup
75
+ Run `incremnt mcp install` to auto-register the server with Claude Desktop and Claude Code (see [Setup](#setup) above).
71
76
 
72
- Add to your Claude Code project config (`.mcp.json`):
77
+ To register manually instead, add to your Claude Code project config (`.mcp.json`):
73
78
 
74
79
  ```json
75
80
  {
76
81
  "mcpServers": {
77
82
  "incremnt": {
78
83
  "type": "stdio",
79
- "command": "npx",
80
- "args": ["-y", "incremnt-mcp"]
84
+ "command": "incremnt-mcp"
81
85
  }
82
86
  }
83
87
  }
84
88
  ```
85
89
 
86
- Or run directly: `npx incremnt-mcp`
87
-
88
90
  The MCP server uses the same auth session as the CLI — run `incremnt login` first.
89
91
 
90
92
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incremnt",
3
- "version": "0.1.13",
3
+ "version": "0.1.16",
4
4
  "description": "Command-line tool for querying your incremnt strength training data",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,7 +13,8 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "test": "node --test",
16
- "dev:sync-fixture": "node ./scripts/dev-sync-fixture-server.js"
16
+ "dev:sync-fixture": "node ./scripts/dev-sync-fixture-server.js",
17
+ "postinstall": "node -e \"process.stdout.write('\\n incremnt MCP server installed.\\n Run: incremnt mcp install\\n to register it with Claude.\\n\\n')\""
17
18
  },
18
19
  "dependencies": {
19
20
  "@modelcontextprotocol/sdk": "^1.12.1",
package/src/contract.js CHANGED
@@ -115,6 +115,31 @@ export const commandSchema = [
115
115
  options: [
116
116
  { name: 'id', type: 'string', required: true, description: 'Cycle summary ID' }
117
117
  ]
118
+ },
119
+ {
120
+ command: 'health summary',
121
+ id: 'health-summary',
122
+ description: 'Health metrics summary (resting HR, HRV, VO2 Max, sleep, cardio)',
123
+ options: [
124
+ { name: 'days', type: 'number', required: false, description: 'Last N days (default: 14)' }
125
+ ]
126
+ },
127
+ {
128
+ command: 'ask history',
129
+ id: 'ask-history',
130
+ description: 'List past AI coach conversations',
131
+ options: [
132
+ { name: 'limit', type: 'number', required: false, description: 'Max conversations to return (default: 20)' }
133
+ ]
134
+ },
135
+ {
136
+ command: 'ask show',
137
+ id: 'ask-show',
138
+ description: 'Show a full AI coach conversation',
139
+ usage: 'ask show --id <conversation-id>',
140
+ options: [
141
+ { name: 'id', type: 'string', required: true, description: 'Conversation ID' }
142
+ ]
118
143
  }
119
144
  ];
120
145
 
package/src/lib.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
- import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { spawn, execFileSync } from 'node:child_process';
3
5
  import { capabilities, commandSchema, contractVersion, officialCommands, proposalSchema, readCommands, writeCommands, writeCommandSchema } from './contract.js';
4
6
  import {
5
7
  bootstrapSessionFromRemoteBaseUrl,
@@ -139,6 +141,40 @@ export async function runCli(argv, stdout, stderr) {
139
141
  }
140
142
  }
141
143
 
144
+ if (normalizedCommand === 'mcp install') {
145
+ try {
146
+ const mcpBin = resolveMcpBin();
147
+ const installed = [];
148
+
149
+ // Claude Desktop
150
+ const desktopPath = claudeDesktopConfigPath();
151
+ let desktopConfig = {};
152
+ try {
153
+ desktopConfig = JSON.parse(await fs.readFile(desktopPath, 'utf8'));
154
+ } catch { /* file doesn't exist yet */ }
155
+ desktopConfig.mcpServers = { ...desktopConfig.mcpServers, incremnt: { command: mcpBin } };
156
+ await fs.mkdir(path.dirname(desktopPath), { recursive: true });
157
+ await fs.writeFile(desktopPath, JSON.stringify(desktopConfig, null, 2) + '\n');
158
+ installed.push(` Claude Desktop: ${desktopPath}`);
159
+
160
+ // Claude Code CLI
161
+ const cliPath = path.join(os.homedir(), '.claude.json');
162
+ let cliConfig = {};
163
+ try {
164
+ cliConfig = JSON.parse(await fs.readFile(cliPath, 'utf8'));
165
+ } catch { /* file doesn't exist yet */ }
166
+ cliConfig.mcpServers = { ...cliConfig.mcpServers, incremnt: { type: 'stdio', command: mcpBin } };
167
+ await fs.writeFile(cliPath, JSON.stringify(cliConfig, null, 2) + '\n');
168
+ installed.push(` Claude Code: ${cliPath}`);
169
+
170
+ stdout.write(`Registered incremnt MCP server in:\n${installed.join('\n')}\n\nRestart Claude for the changes to take effect.\n`);
171
+ return 0;
172
+ } catch (error) {
173
+ stderr.write(`${error.message}\n`);
174
+ return 1;
175
+ }
176
+ }
177
+
142
178
  if (normalizedCommand === 'login') {
143
179
  if (options.snapshot) {
144
180
  try {
@@ -380,6 +416,26 @@ function browserCommandForPlatform() {
380
416
  return null;
381
417
  }
382
418
 
419
+ function claudeDesktopConfigPath() {
420
+ if (process.platform === 'darwin') {
421
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
422
+ }
423
+ if (process.platform === 'win32') {
424
+ return path.join(process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
425
+ }
426
+ return path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json');
427
+ }
428
+
429
+ function resolveMcpBin() {
430
+ try {
431
+ return execFileSync('which', ['incremnt-mcp'], { encoding: 'utf8' }).trim();
432
+ } catch {
433
+ // fall back to sibling binary next to the current incremnt binary
434
+ const dir = path.dirname(process.argv[1]);
435
+ return path.join(dir, 'incremnt-mcp');
436
+ }
437
+ }
438
+
383
439
  function configuredAuthProviders(auth) {
384
440
  const providers = auth?.providers ?? {};
385
441
  const labels = [];
package/src/mcp.js CHANGED
@@ -1,18 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
3
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
7
  import { z } from 'zod';
6
- import { commandSchema } from './contract.js';
8
+ import { commandSchema, writeCommands, writeCommandSchema } from './contract.js';
7
9
  import { readSessionState } from './state.js';
8
10
  import { createTransport } from './transport.js';
9
11
 
10
- const server = new McpServer({
11
- name: 'incremnt',
12
- version: '0.1.0'
13
- });
14
-
15
- for (const cmd of commandSchema) {
12
+ function commandShape(cmd) {
16
13
  const shape = {};
17
14
 
18
15
  for (const opt of cmd.options) {
@@ -22,39 +19,69 @@ for (const cmd of commandSchema) {
22
19
  shape[opt.name] = field;
23
20
  }
24
21
 
25
- server.tool(cmd.id, cmd.description, shape, async (args) => {
26
- try {
27
- const sessionState = await readSessionState();
28
- const transport = await createTransport({}, sessionState);
22
+ return shape;
23
+ }
24
+
25
+ export function registerMcpTools(server, {
26
+ readSessionStateFn = readSessionState,
27
+ createTransportFn = createTransport
28
+ } = {}) {
29
+ for (const cmd of [...commandSchema, ...writeCommandSchema]) {
30
+ server.tool(cmd.id, cmd.description, commandShape(cmd), async (args) => {
31
+ try {
32
+ const sessionState = await readSessionStateFn();
33
+ const transport = await createTransportFn({}, sessionState);
34
+
35
+ if (transport.expired) {
36
+ return {
37
+ content: [{ type: 'text', text: 'Session expired. Run `incremnt login` to re-authenticate.' }],
38
+ isError: true
39
+ };
40
+ }
41
+
42
+ const result = writeCommands.has(cmd.id)
43
+ ? await transport.executeWriteCommand(cmd.id, args)
44
+ : await transport.executeReadCommand(cmd.id, args);
29
45
 
30
- if (transport.expired) {
31
46
  return {
32
- content: [{ type: 'text', text: 'Session expired. Run `incremnt login` to re-authenticate.' }],
33
- isError: true
47
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
34
48
  };
35
- }
49
+ } catch (error) {
50
+ const message = error?.message ?? String(error);
36
51
 
37
- const result = await transport.executeReadCommand(cmd.id, args);
38
- return {
39
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
40
- };
41
- } catch (error) {
42
- const message = error?.message ?? String(error);
52
+ if (error?.code === 'SNAPSHOT_NOT_FOUND') {
53
+ return {
54
+ content: [{ type: 'text', text: 'Not logged in. Run `incremnt login` first.' }],
55
+ isError: true
56
+ };
57
+ }
43
58
 
44
- if (error?.code === 'SNAPSHOT_NOT_FOUND') {
45
59
  return {
46
- content: [{ type: 'text', text: 'Not logged in. Run `incremnt login` first.' }],
60
+ content: [{ type: 'text', text: message }],
47
61
  isError: true
48
62
  };
49
63
  }
64
+ });
65
+ }
66
+
67
+ return server;
68
+ }
50
69
 
51
- return {
52
- content: [{ type: 'text', text: message }],
53
- isError: true
54
- };
55
- }
70
+ export function createMcpServer(deps) {
71
+ const server = new McpServer({
72
+ name: 'incremnt',
73
+ version: '0.1.0'
56
74
  });
75
+
76
+ return registerMcpTools(server, deps);
57
77
  }
58
78
 
59
- const transport = new StdioServerTransport();
60
- await server.connect(transport);
79
+ async function main() {
80
+ const transport = new StdioServerTransport();
81
+ const server = createMcpServer();
82
+ await server.connect(transport);
83
+ }
84
+
85
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
86
+ await main();
87
+ }