@1medium/cli 1.0.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 +158 -0
- package/package.json +47 -0
- package/src/api.js +139 -0
- package/src/config.js +40 -0
- package/src/index.js +487 -0
- package/src/mcp-server.js +330 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @1medium/cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for 1Medium AI coding agent integration.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# From npm (when published)
|
|
9
|
+
npm install -g @1medium/cli
|
|
10
|
+
|
|
11
|
+
# From source
|
|
12
|
+
cd packages/cli
|
|
13
|
+
npm install
|
|
14
|
+
npm link
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
Set your API token:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Interactive
|
|
23
|
+
1m login
|
|
24
|
+
|
|
25
|
+
# With token directly
|
|
26
|
+
1m login --token 1m_pat_xxxxxxxxxxxxx
|
|
27
|
+
|
|
28
|
+
# Or via environment variable
|
|
29
|
+
export ONEMEDIUM_TOKEN=1m_pat_xxxxxxxxxxxxx
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Authentication
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Check current token and rate limits
|
|
38
|
+
1m whoami
|
|
39
|
+
|
|
40
|
+
# View configuration
|
|
41
|
+
1m config
|
|
42
|
+
|
|
43
|
+
# Remove credentials
|
|
44
|
+
1m logout
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Task Management
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Create a task
|
|
51
|
+
1m task add --title "Fix authentication bug in login flow" \
|
|
52
|
+
--body "The login form fails when..." \
|
|
53
|
+
--priority P1 \
|
|
54
|
+
--repo owner/repo \
|
|
55
|
+
--branch feature/auth-fix
|
|
56
|
+
|
|
57
|
+
# List tasks
|
|
58
|
+
1m task list
|
|
59
|
+
1m task list --status open --priority P0
|
|
60
|
+
1m task list --repo owner/repo --created-by agent
|
|
61
|
+
|
|
62
|
+
# Get task details
|
|
63
|
+
1m task get <task-id>
|
|
64
|
+
|
|
65
|
+
# Update a task
|
|
66
|
+
1m task update <task-id> --status doing --add-tag in-progress
|
|
67
|
+
|
|
68
|
+
# Add a comment
|
|
69
|
+
1m task comment <task-id> --message "Started working on this"
|
|
70
|
+
|
|
71
|
+
# Mark as complete
|
|
72
|
+
1m task done <task-id> --message "Fixed in commit abc123"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### JSON Output
|
|
76
|
+
|
|
77
|
+
All commands support `--json` flag for machine-readable output:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
1m task list --json
|
|
81
|
+
1m task get <id> --json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Environment Variables
|
|
85
|
+
|
|
86
|
+
- `ONEMEDIUM_TOKEN` - API token (overrides stored config)
|
|
87
|
+
- `ONEMEDIUM_API_URL` - API base URL (default: https://1medium.ai/api)
|
|
88
|
+
|
|
89
|
+
## MCP Server (Claude Code Integration)
|
|
90
|
+
|
|
91
|
+
This package includes an MCP (Model Context Protocol) server for direct integration with Claude Code.
|
|
92
|
+
|
|
93
|
+
### Setup
|
|
94
|
+
|
|
95
|
+
Add to your Claude Code MCP config (`~/.claude.json`):
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"1medium": {
|
|
101
|
+
"command": "1m-mcp"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Restart Claude Code, and it will have access to these tools:
|
|
108
|
+
|
|
109
|
+
| Tool | Description |
|
|
110
|
+
|------|-------------|
|
|
111
|
+
| `task_create` | Create tasks with title, body, priority |
|
|
112
|
+
| `task_list` | List tasks filtered by status/priority |
|
|
113
|
+
| `task_get` | Get full task details |
|
|
114
|
+
| `task_update` | Update task properties |
|
|
115
|
+
| `task_complete` | Mark task as done |
|
|
116
|
+
| `task_comment` | Add comments/updates |
|
|
117
|
+
|
|
118
|
+
### Usage
|
|
119
|
+
|
|
120
|
+
Once configured, ask Claude Code things like:
|
|
121
|
+
|
|
122
|
+
- "Create a task to fix the authentication bug"
|
|
123
|
+
- "Show me my P1 tasks"
|
|
124
|
+
- "Mark task ABC as complete"
|
|
125
|
+
- "Add a comment to task XYZ saying I'm working on it"
|
|
126
|
+
|
|
127
|
+
## For AI Agents
|
|
128
|
+
|
|
129
|
+
The CLI is designed for use by AI coding agents like Claude Code, Cursor, and Copilot.
|
|
130
|
+
|
|
131
|
+
Example workflow for an AI agent:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Agent creates task for discovered issue
|
|
135
|
+
1m task add --title "Security: SQL injection in user search" \
|
|
136
|
+
--body "Found potential SQL injection in /api/users/search..." \
|
|
137
|
+
--priority P0 \
|
|
138
|
+
--repo myorg/myapp \
|
|
139
|
+
--file "src/api/users.js" \
|
|
140
|
+
--source "claude-code"
|
|
141
|
+
|
|
142
|
+
# Agent updates progress
|
|
143
|
+
1m task comment <id> --message "Implemented parameterized queries"
|
|
144
|
+
|
|
145
|
+
# Agent marks complete
|
|
146
|
+
1m task done <id> --message "Fixed in PR #123"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Getting Your API Token
|
|
150
|
+
|
|
151
|
+
1. Go to [1medium.ai](https://1medium.ai)
|
|
152
|
+
2. Navigate to Settings > API Tokens
|
|
153
|
+
3. Create a new agent token
|
|
154
|
+
4. Run `1m login --token YOUR_TOKEN`
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@1medium/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI and MCP server for 1Medium AI task management",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"1m": "./src/index.js",
|
|
8
|
+
"1m-mcp": "./src/mcp-server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"1medium",
|
|
18
|
+
"cli",
|
|
19
|
+
"mcp",
|
|
20
|
+
"claude",
|
|
21
|
+
"agent",
|
|
22
|
+
"task-management",
|
|
23
|
+
"model-context-protocol"
|
|
24
|
+
],
|
|
25
|
+
"author": "1Medium <support@1medium.ai>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/1medium/cli.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://1medium.ai",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/1medium/cli/issues"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
40
|
+
"chalk": "^4.1.2",
|
|
41
|
+
"commander": "^12.0.0",
|
|
42
|
+
"conf": "^10.2.0",
|
|
43
|
+
"dotenv": "^16.3.1",
|
|
44
|
+
"node-fetch": "^2.7.0",
|
|
45
|
+
"ora": "^5.4.1"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fetch = require("node-fetch");
|
|
4
|
+
const config = require("./config");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get API base URL
|
|
8
|
+
*/
|
|
9
|
+
function getApiUrl() {
|
|
10
|
+
return config.get("apiUrl") || "https://1medium.ai/api";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get auth token
|
|
15
|
+
*/
|
|
16
|
+
function getToken() {
|
|
17
|
+
const token = config.get("token");
|
|
18
|
+
if (!token) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"No token configured. Run '1m login' to set your API token."
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return token;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Make an authenticated API request
|
|
28
|
+
*/
|
|
29
|
+
async function request(method, path, body = null, params = null) {
|
|
30
|
+
const baseUrl = getApiUrl();
|
|
31
|
+
const token = getToken();
|
|
32
|
+
|
|
33
|
+
let url = `${baseUrl}/v1/agent${path}`;
|
|
34
|
+
if (params) {
|
|
35
|
+
const searchParams = new URLSearchParams();
|
|
36
|
+
for (const [key, value] of Object.entries(params)) {
|
|
37
|
+
if (value !== undefined && value !== null) {
|
|
38
|
+
searchParams.append(key, value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const queryString = searchParams.toString();
|
|
42
|
+
if (queryString) {
|
|
43
|
+
url += `?${queryString}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const options = {
|
|
48
|
+
method,
|
|
49
|
+
headers: {
|
|
50
|
+
Authorization: `Bearer ${token}`,
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
"User-Agent": "1m-cli/1.0.0",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (body && (method === "POST" || method === "PATCH" || method === "PUT")) {
|
|
57
|
+
options.body = JSON.stringify(body);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const response = await fetch(url, options);
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const message = data.message || data.error || "API request failed";
|
|
65
|
+
const error = new Error(message);
|
|
66
|
+
error.statusCode = response.status;
|
|
67
|
+
error.details = data.details;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Token introspection
|
|
76
|
+
*/
|
|
77
|
+
async function whoami() {
|
|
78
|
+
return request("GET", "/whoami");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create a task
|
|
83
|
+
*/
|
|
84
|
+
async function createTask(payload) {
|
|
85
|
+
return request("POST", "/tasks", payload);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* List tasks
|
|
90
|
+
*/
|
|
91
|
+
async function listTasks(params = {}) {
|
|
92
|
+
return request("GET", "/tasks", null, params);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get a single task
|
|
97
|
+
*/
|
|
98
|
+
async function getTask(id) {
|
|
99
|
+
return request("GET", `/tasks/${id}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Update a task
|
|
104
|
+
*/
|
|
105
|
+
async function updateTask(id, payload) {
|
|
106
|
+
return request("PATCH", `/tasks/${id}`, payload);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Add a comment to a task
|
|
111
|
+
*/
|
|
112
|
+
async function addComment(taskId, payload) {
|
|
113
|
+
return request("POST", `/tasks/${taskId}/comments`, payload);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Complete a task
|
|
118
|
+
*/
|
|
119
|
+
async function completeTask(taskId, payload = {}) {
|
|
120
|
+
return request("POST", `/tasks/${taskId}/complete`, payload);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* List organizations
|
|
125
|
+
*/
|
|
126
|
+
async function listOrgs() {
|
|
127
|
+
return request("GET", "/orgs");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
whoami,
|
|
132
|
+
createTask,
|
|
133
|
+
listTasks,
|
|
134
|
+
getTask,
|
|
135
|
+
updateTask,
|
|
136
|
+
addComment,
|
|
137
|
+
completeTask,
|
|
138
|
+
listOrgs,
|
|
139
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const Conf = require("conf");
|
|
4
|
+
|
|
5
|
+
const config = new Conf({
|
|
6
|
+
projectName: "1medium-cli",
|
|
7
|
+
schema: {
|
|
8
|
+
token: {
|
|
9
|
+
type: "string",
|
|
10
|
+
default: "",
|
|
11
|
+
},
|
|
12
|
+
apiUrl: {
|
|
13
|
+
type: "string",
|
|
14
|
+
default: "https://1medium.ai/api",
|
|
15
|
+
},
|
|
16
|
+
orgId: {
|
|
17
|
+
type: "string",
|
|
18
|
+
default: "",
|
|
19
|
+
},
|
|
20
|
+
orgName: {
|
|
21
|
+
type: "string",
|
|
22
|
+
default: "",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Allow environment variable override
|
|
28
|
+
if (process.env.ONEMEDIUM_TOKEN) {
|
|
29
|
+
config.set("token", process.env.ONEMEDIUM_TOKEN);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (process.env.ONEMEDIUM_API_URL) {
|
|
33
|
+
config.set("apiUrl", process.env.ONEMEDIUM_API_URL);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (process.env.ONEMEDIUM_ORG_ID) {
|
|
37
|
+
config.set("orgId", process.env.ONEMEDIUM_ORG_ID);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = config;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { Command } = require("commander");
|
|
5
|
+
const chalk = require("chalk");
|
|
6
|
+
const { version } = require("../package.json");
|
|
7
|
+
const config = require("./config");
|
|
8
|
+
const api = require("./api");
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name("1m")
|
|
14
|
+
.description("1Medium CLI for AI coding agent integration")
|
|
15
|
+
.version(version);
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Auth Commands
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.command("login")
|
|
23
|
+
.description("Configure your 1Medium API token")
|
|
24
|
+
.option("-t, --token <token>", "API token (1m_pat_xxx)")
|
|
25
|
+
.option("-u, --url <url>", "API URL (default: https://1medium.ai/api)")
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
try {
|
|
28
|
+
if (options.token) {
|
|
29
|
+
if (!options.token.startsWith("1m_pat_")) {
|
|
30
|
+
console.error(chalk.red("Error: Token must start with 1m_pat_"));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
config.set("token", options.token);
|
|
34
|
+
console.log(chalk.green("Token saved successfully!"));
|
|
35
|
+
} else {
|
|
36
|
+
// Interactive mode - prompt for token
|
|
37
|
+
const readline = require("readline");
|
|
38
|
+
const rl = readline.createInterface({
|
|
39
|
+
input: process.stdin,
|
|
40
|
+
output: process.stdout,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
rl.question("Enter your 1Medium API token (1m_pat_xxx): ", (token) => {
|
|
44
|
+
rl.close();
|
|
45
|
+
if (!token.startsWith("1m_pat_")) {
|
|
46
|
+
console.error(chalk.red("Error: Token must start with 1m_pat_"));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
config.set("token", token);
|
|
50
|
+
console.log(chalk.green("Token saved successfully!"));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (options.url) {
|
|
55
|
+
config.set("apiUrl", options.url);
|
|
56
|
+
console.log(chalk.green(`API URL set to: ${options.url}`));
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
program
|
|
65
|
+
.command("logout")
|
|
66
|
+
.description("Remove stored credentials")
|
|
67
|
+
.action(() => {
|
|
68
|
+
config.clear();
|
|
69
|
+
console.log(chalk.green("Credentials removed."));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command("whoami")
|
|
74
|
+
.description("Display current token info and rate limits")
|
|
75
|
+
.action(async () => {
|
|
76
|
+
try {
|
|
77
|
+
const data = await api.whoami();
|
|
78
|
+
console.log(chalk.bold("\nToken Info:"));
|
|
79
|
+
console.log(` Name: ${data.token.name}`);
|
|
80
|
+
console.log(` Prefix: ${data.token.prefix}`);
|
|
81
|
+
console.log(` Scopes: ${data.token.scopes.join(", ")}`);
|
|
82
|
+
if (data.token.repo_allowlist) {
|
|
83
|
+
console.log(` Repos: ${data.token.repo_allowlist.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
console.log(chalk.bold("\nRate Limits:"));
|
|
86
|
+
console.log(` Requests/min: ${data.rate_limits.requests_per_minute}`);
|
|
87
|
+
console.log(` Creates/hour: ${data.rate_limits.creates_per_hour}`);
|
|
88
|
+
console.log(
|
|
89
|
+
` Limited: ${data.rate_limits.request_limited ? chalk.red("Yes") : chalk.green("No")}`
|
|
90
|
+
);
|
|
91
|
+
console.log(chalk.bold("\nUser:"));
|
|
92
|
+
console.log(` Email: ${data.user.email}`);
|
|
93
|
+
console.log(` Username: ${data.user.username}`);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Task Commands
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
const taskCmd = program.command("task").description("Manage tasks");
|
|
105
|
+
|
|
106
|
+
taskCmd
|
|
107
|
+
.command("add")
|
|
108
|
+
.description("Create a new task")
|
|
109
|
+
.requiredOption("-t, --title <title>", "Task title (min 10 chars)")
|
|
110
|
+
.option("-b, --body <body>", "Task body (markdown)")
|
|
111
|
+
.option("-p, --priority <priority>", "Priority: P0, P1, P2, P3", "P2")
|
|
112
|
+
.option("-s, --status <status>", "Status: inbox, open, doing, blocked, done", "inbox")
|
|
113
|
+
.option("--tag <tags...>", "Tags to add")
|
|
114
|
+
.option("--repo <repo>", "Repository context (owner/repo)")
|
|
115
|
+
.option("--branch <branch>", "Branch context")
|
|
116
|
+
.option("--commit <commit>", "Commit SHA context")
|
|
117
|
+
.option("--file <files...>", "File paths context")
|
|
118
|
+
.option("--pr <url>", "PR URL context")
|
|
119
|
+
.option("--issue <url>", "Issue URL context")
|
|
120
|
+
.option("--estimate <minutes>", "Time estimate in minutes", parseInt)
|
|
121
|
+
.option("--due <date>", "Due date (ISO 8601)")
|
|
122
|
+
.option("--source <source>", "Source identifier", "cli")
|
|
123
|
+
.option("--no-dedupe", "Disable deduplication")
|
|
124
|
+
.option("-j, --json", "Output as JSON")
|
|
125
|
+
.action(async (options) => {
|
|
126
|
+
try {
|
|
127
|
+
const context = {};
|
|
128
|
+
if (options.repo) context.repo = options.repo;
|
|
129
|
+
if (options.branch) context.branch = options.branch;
|
|
130
|
+
if (options.commit) context.commit = options.commit;
|
|
131
|
+
if (options.file) context.files = options.file;
|
|
132
|
+
if (options.pr) context.pr_url = options.pr;
|
|
133
|
+
if (options.issue) context.issue_url = options.issue;
|
|
134
|
+
|
|
135
|
+
const payload = {
|
|
136
|
+
title: options.title,
|
|
137
|
+
body_md: options.body,
|
|
138
|
+
priority: options.priority,
|
|
139
|
+
status: options.status,
|
|
140
|
+
tags: options.tag || [],
|
|
141
|
+
context,
|
|
142
|
+
source: options.source,
|
|
143
|
+
estimate_minutes: options.estimate,
|
|
144
|
+
due_at: options.due,
|
|
145
|
+
dedupe: {
|
|
146
|
+
mode: options.dedupe === false ? "none" : "auto",
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const data = await api.createTask(payload);
|
|
151
|
+
|
|
152
|
+
if (options.json) {
|
|
153
|
+
console.log(JSON.stringify(data, null, 2));
|
|
154
|
+
} else {
|
|
155
|
+
if (data.deduped) {
|
|
156
|
+
console.log(chalk.yellow("Similar task already exists (deduped):"));
|
|
157
|
+
} else {
|
|
158
|
+
console.log(chalk.green("Task created:"));
|
|
159
|
+
}
|
|
160
|
+
console.log(` ID: ${data.task.id}`);
|
|
161
|
+
console.log(` Title: ${data.task.title}`);
|
|
162
|
+
console.log(` Status: ${data.task.status}`);
|
|
163
|
+
console.log(` Priority: ${data.task.priority}`);
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
167
|
+
if (error.details) {
|
|
168
|
+
error.details.forEach((d) => console.error(chalk.red(` - ${d}`)));
|
|
169
|
+
}
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
taskCmd
|
|
175
|
+
.command("list")
|
|
176
|
+
.description("List tasks")
|
|
177
|
+
.option("-s, --status <status>", "Filter by status")
|
|
178
|
+
.option("-p, --priority <priority>", "Filter by priority")
|
|
179
|
+
.option("--repo <repo>", "Filter by repository")
|
|
180
|
+
.option("--tag <tag>", "Filter by tag")
|
|
181
|
+
.option("--created-by <type>", "Filter by creator: all, agent, user", "all")
|
|
182
|
+
.option("-l, --limit <limit>", "Number of tasks to return", "20")
|
|
183
|
+
.option("-o, --offset <offset>", "Offset for pagination", "0")
|
|
184
|
+
.option("-j, --json", "Output as JSON")
|
|
185
|
+
.action(async (options) => {
|
|
186
|
+
try {
|
|
187
|
+
const params = {
|
|
188
|
+
status: options.status,
|
|
189
|
+
priority: options.priority,
|
|
190
|
+
repo: options.repo,
|
|
191
|
+
tag: options.tag,
|
|
192
|
+
created_by: options.createdBy,
|
|
193
|
+
limit: options.limit,
|
|
194
|
+
offset: options.offset,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const data = await api.listTasks(params);
|
|
198
|
+
|
|
199
|
+
if (options.json) {
|
|
200
|
+
console.log(JSON.stringify(data, null, 2));
|
|
201
|
+
} else {
|
|
202
|
+
console.log(chalk.bold(`\nTasks (${data.total} total):\n`));
|
|
203
|
+
if (data.tasks.length === 0) {
|
|
204
|
+
console.log(" No tasks found.");
|
|
205
|
+
} else {
|
|
206
|
+
for (const task of data.tasks) {
|
|
207
|
+
const priorityColor =
|
|
208
|
+
task.priority === "P0"
|
|
209
|
+
? chalk.red
|
|
210
|
+
: task.priority === "P1"
|
|
211
|
+
? chalk.yellow
|
|
212
|
+
: chalk.white;
|
|
213
|
+
const statusIcon =
|
|
214
|
+
task.status === "done"
|
|
215
|
+
? chalk.green("[x]")
|
|
216
|
+
: task.status === "doing"
|
|
217
|
+
? chalk.blue("[>]")
|
|
218
|
+
: task.status === "blocked"
|
|
219
|
+
? chalk.red("[!]")
|
|
220
|
+
: "[ ]";
|
|
221
|
+
|
|
222
|
+
console.log(
|
|
223
|
+
`${statusIcon} ${priorityColor(task.priority)} ${task.title.substring(0, 60)}${task.title.length > 60 ? "..." : ""}`
|
|
224
|
+
);
|
|
225
|
+
console.log(chalk.gray(` ID: ${task.id}`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
console.log("");
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
taskCmd
|
|
237
|
+
.command("get <id>")
|
|
238
|
+
.description("Get task details")
|
|
239
|
+
.option("-j, --json", "Output as JSON")
|
|
240
|
+
.action(async (id, options) => {
|
|
241
|
+
try {
|
|
242
|
+
const data = await api.getTask(id);
|
|
243
|
+
|
|
244
|
+
if (options.json) {
|
|
245
|
+
console.log(JSON.stringify(data, null, 2));
|
|
246
|
+
} else {
|
|
247
|
+
const task = data.task;
|
|
248
|
+
console.log(chalk.bold(`\n${task.title}\n`));
|
|
249
|
+
console.log(` ID: ${task.id}`);
|
|
250
|
+
console.log(` Status: ${task.status}`);
|
|
251
|
+
console.log(` Priority: ${task.priority}`);
|
|
252
|
+
if (task.tags?.length) {
|
|
253
|
+
console.log(` Tags: ${task.tags.join(", ")}`);
|
|
254
|
+
}
|
|
255
|
+
if (task.due_at) {
|
|
256
|
+
console.log(` Due: ${task.due_at}`);
|
|
257
|
+
}
|
|
258
|
+
if (task.estimate_minutes) {
|
|
259
|
+
console.log(` Estimate: ${task.estimate_minutes} minutes`);
|
|
260
|
+
}
|
|
261
|
+
if (task.context) {
|
|
262
|
+
console.log(chalk.bold("\n Context:"));
|
|
263
|
+
if (task.context.repo) console.log(` Repo: ${task.context.repo}`);
|
|
264
|
+
if (task.context.branch) console.log(` Branch: ${task.context.branch}`);
|
|
265
|
+
if (task.context.commit) console.log(` Commit: ${task.context.commit}`);
|
|
266
|
+
if (task.context.pr_url) console.log(` PR: ${task.context.pr_url}`);
|
|
267
|
+
}
|
|
268
|
+
if (task.body_md) {
|
|
269
|
+
console.log(chalk.bold("\n Description:"));
|
|
270
|
+
console.log(` ${task.body_md.replace(/\n/g, "\n ")}`);
|
|
271
|
+
}
|
|
272
|
+
if (task.comments?.length) {
|
|
273
|
+
console.log(chalk.bold("\n Comments:"));
|
|
274
|
+
for (const comment of task.comments) {
|
|
275
|
+
console.log(chalk.gray(` [${comment.created_at}]`));
|
|
276
|
+
console.log(` ${comment.content.replace(/\n/g, "\n ")}`);
|
|
277
|
+
console.log("");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
taskCmd
|
|
288
|
+
.command("update <id>")
|
|
289
|
+
.description("Update a task")
|
|
290
|
+
.option("-t, --title <title>", "New title")
|
|
291
|
+
.option("-b, --body <body>", "New body (markdown)")
|
|
292
|
+
.option("-p, --priority <priority>", "New priority: P0, P1, P2, P3")
|
|
293
|
+
.option("-s, --status <status>", "New status")
|
|
294
|
+
.option("--add-tag <tags...>", "Tags to add")
|
|
295
|
+
.option("--remove-tag <tags...>", "Tags to remove")
|
|
296
|
+
.option("--due <date>", "Due date (ISO 8601)")
|
|
297
|
+
.option("--estimate <minutes>", "Time estimate in minutes", parseInt)
|
|
298
|
+
.option("-j, --json", "Output as JSON")
|
|
299
|
+
.action(async (id, options) => {
|
|
300
|
+
try {
|
|
301
|
+
const payload = {};
|
|
302
|
+
if (options.title) payload.title = options.title;
|
|
303
|
+
if (options.body) payload.body_md = options.body;
|
|
304
|
+
if (options.priority) payload.priority = options.priority;
|
|
305
|
+
if (options.status) payload.status = options.status;
|
|
306
|
+
if (options.addTag) payload.tags_add = options.addTag;
|
|
307
|
+
if (options.removeTag) payload.tags_remove = options.removeTag;
|
|
308
|
+
if (options.due) payload.due_at = options.due;
|
|
309
|
+
if (options.estimate) payload.estimate_minutes = options.estimate;
|
|
310
|
+
|
|
311
|
+
const data = await api.updateTask(id, payload);
|
|
312
|
+
|
|
313
|
+
if (options.json) {
|
|
314
|
+
console.log(JSON.stringify(data, null, 2));
|
|
315
|
+
} else {
|
|
316
|
+
console.log(chalk.green("Task updated:"));
|
|
317
|
+
console.log(` ID: ${data.task.id}`);
|
|
318
|
+
console.log(` Title: ${data.task.title}`);
|
|
319
|
+
console.log(` Status: ${data.task.status}`);
|
|
320
|
+
console.log(` Priority: ${data.task.priority}`);
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
taskCmd
|
|
329
|
+
.command("comment <id>")
|
|
330
|
+
.description("Add a comment to a task")
|
|
331
|
+
.requiredOption("-m, --message <message>", "Comment message (markdown)")
|
|
332
|
+
.option("-j, --json", "Output as JSON")
|
|
333
|
+
.action(async (id, options) => {
|
|
334
|
+
try {
|
|
335
|
+
const data = await api.addComment(id, { comment_md: options.message });
|
|
336
|
+
|
|
337
|
+
if (options.json) {
|
|
338
|
+
console.log(JSON.stringify(data, null, 2));
|
|
339
|
+
} else {
|
|
340
|
+
console.log(chalk.green("Comment added:"));
|
|
341
|
+
console.log(` ID: ${data.comment.id}`);
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
taskCmd
|
|
350
|
+
.command("done <id>")
|
|
351
|
+
.description("Mark a task as complete")
|
|
352
|
+
.option("-m, --message <message>", "Completion summary (markdown)")
|
|
353
|
+
.option("-j, --json", "Output as JSON")
|
|
354
|
+
.action(async (id, options) => {
|
|
355
|
+
try {
|
|
356
|
+
const payload = {};
|
|
357
|
+
if (options.message) payload.summary_md = options.message;
|
|
358
|
+
|
|
359
|
+
const data = await api.completeTask(id, payload);
|
|
360
|
+
|
|
361
|
+
if (options.json) {
|
|
362
|
+
console.log(JSON.stringify(data, null, 2));
|
|
363
|
+
} else {
|
|
364
|
+
console.log(chalk.green("Task completed!"));
|
|
365
|
+
console.log(` ID: ${data.task.id}`);
|
|
366
|
+
console.log(` Title: ${data.task.title}`);
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ============================================================================
|
|
375
|
+
// Org Commands
|
|
376
|
+
// ============================================================================
|
|
377
|
+
|
|
378
|
+
program
|
|
379
|
+
.command("orgs")
|
|
380
|
+
.description("List your organizations")
|
|
381
|
+
.option("-j, --json", "Output as JSON")
|
|
382
|
+
.action(async (options) => {
|
|
383
|
+
try {
|
|
384
|
+
const data = await api.listOrgs();
|
|
385
|
+
const currentOrgId = config.get("orgId");
|
|
386
|
+
|
|
387
|
+
if (options.json) {
|
|
388
|
+
console.log(JSON.stringify(data, null, 2));
|
|
389
|
+
} else {
|
|
390
|
+
console.log(chalk.bold("\nOrganizations:\n"));
|
|
391
|
+
for (const org of data.orgs) {
|
|
392
|
+
const current = org.id === currentOrgId ? chalk.green(" (current)") : "";
|
|
393
|
+
const personal = org.is_personal ? chalk.gray(" [personal]") : "";
|
|
394
|
+
console.log(` ${org.name}${personal}${current}`);
|
|
395
|
+
console.log(chalk.gray(` ID: ${org.id}`));
|
|
396
|
+
}
|
|
397
|
+
console.log("");
|
|
398
|
+
if (!currentOrgId) {
|
|
399
|
+
console.log(chalk.yellow("No default org set. Use: 1m config set org <org-id>"));
|
|
400
|
+
console.log("");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ============================================================================
|
|
410
|
+
// Config Commands
|
|
411
|
+
// ============================================================================
|
|
412
|
+
|
|
413
|
+
const configCmd = program.command("config").description("Manage configuration");
|
|
414
|
+
|
|
415
|
+
configCmd
|
|
416
|
+
.command("show")
|
|
417
|
+
.description("Show current configuration")
|
|
418
|
+
.action(() => {
|
|
419
|
+
const token = config.get("token");
|
|
420
|
+
const apiUrl = config.get("apiUrl");
|
|
421
|
+
const orgId = config.get("orgId");
|
|
422
|
+
const orgName = config.get("orgName");
|
|
423
|
+
|
|
424
|
+
console.log(chalk.bold("\nConfiguration:\n"));
|
|
425
|
+
console.log(` API URL: ${apiUrl || "https://1medium.ai/api (default)"}`);
|
|
426
|
+
console.log(` Token: ${token ? token.substring(0, 12) + "..." : chalk.yellow("Not set")}`);
|
|
427
|
+
console.log(` Org: ${orgName || chalk.yellow("Not set")}${orgId ? chalk.gray(` (${orgId})`) : ""}`);
|
|
428
|
+
console.log(` Config: ${config.path}`);
|
|
429
|
+
console.log("");
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
configCmd
|
|
433
|
+
.command("set")
|
|
434
|
+
.description("Set a configuration value")
|
|
435
|
+
.argument("<key>", "Configuration key (org, url)")
|
|
436
|
+
.argument("<value>", "Configuration value")
|
|
437
|
+
.action(async (key, value) => {
|
|
438
|
+
try {
|
|
439
|
+
switch (key) {
|
|
440
|
+
case "org":
|
|
441
|
+
// Validate org exists and user has access
|
|
442
|
+
const data = await api.listOrgs();
|
|
443
|
+
const org = data.orgs.find((o) => o.id === value || o.name.toLowerCase() === value.toLowerCase());
|
|
444
|
+
if (!org) {
|
|
445
|
+
console.error(chalk.red(`Error: Organization not found: ${value}`));
|
|
446
|
+
console.log("\nAvailable organizations:");
|
|
447
|
+
for (const o of data.orgs) {
|
|
448
|
+
console.log(` ${o.name} (${o.id})`);
|
|
449
|
+
}
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
config.set("orgId", org.id);
|
|
453
|
+
config.set("orgName", org.name);
|
|
454
|
+
console.log(chalk.green(`Default org set to: ${org.name}`));
|
|
455
|
+
break;
|
|
456
|
+
case "url":
|
|
457
|
+
config.set("apiUrl", value);
|
|
458
|
+
console.log(chalk.green(`API URL set to: ${value}`));
|
|
459
|
+
break;
|
|
460
|
+
default:
|
|
461
|
+
console.error(chalk.red(`Unknown config key: ${key}`));
|
|
462
|
+
console.log("Available keys: org, url");
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
} catch (error) {
|
|
466
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Default action for `1m config` (no subcommand) - show config
|
|
472
|
+
configCmd.action(() => {
|
|
473
|
+
const token = config.get("token");
|
|
474
|
+
const apiUrl = config.get("apiUrl");
|
|
475
|
+
const orgId = config.get("orgId");
|
|
476
|
+
const orgName = config.get("orgName");
|
|
477
|
+
|
|
478
|
+
console.log(chalk.bold("\nConfiguration:\n"));
|
|
479
|
+
console.log(` API URL: ${apiUrl || "https://1medium.ai/api (default)"}`);
|
|
480
|
+
console.log(` Token: ${token ? token.substring(0, 12) + "..." : chalk.yellow("Not set")}`);
|
|
481
|
+
console.log(` Org: ${orgName || chalk.yellow("Not set")}${orgId ? chalk.gray(` (${orgId})`) : ""}`);
|
|
482
|
+
console.log(` Config: ${config.path}`);
|
|
483
|
+
console.log("");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Parse and execute
|
|
487
|
+
program.parse();
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
|
+
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema,
|
|
9
|
+
} = require("@modelcontextprotocol/sdk/types.js");
|
|
10
|
+
const api = require("./api");
|
|
11
|
+
const config = require("./config");
|
|
12
|
+
|
|
13
|
+
const server = new Server(
|
|
14
|
+
{
|
|
15
|
+
name: "1medium",
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
capabilities: {
|
|
20
|
+
tools: {},
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Define available tools
|
|
26
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
27
|
+
return {
|
|
28
|
+
tools: [
|
|
29
|
+
{
|
|
30
|
+
name: "task_create",
|
|
31
|
+
description: "Create a new task in 1Medium. Use this to track work items, todos, or any actionable items.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
title: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "The task title/summary",
|
|
38
|
+
},
|
|
39
|
+
body: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Optional detailed description in markdown",
|
|
42
|
+
},
|
|
43
|
+
priority: {
|
|
44
|
+
type: "string",
|
|
45
|
+
enum: ["P1", "P2", "P3", "P4"],
|
|
46
|
+
description: "Task priority (P1=urgent, P4=low)",
|
|
47
|
+
},
|
|
48
|
+
source: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Source identifier (e.g., 'claude-code', 'github')",
|
|
51
|
+
},
|
|
52
|
+
context: {
|
|
53
|
+
type: "object",
|
|
54
|
+
description: "Additional context like repo, branch, file paths",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required: ["title"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "task_list",
|
|
62
|
+
description: "List tasks from 1Medium. Returns open tasks by default.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
status: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: ["open", "completed", "all"],
|
|
69
|
+
description: "Filter by task status",
|
|
70
|
+
},
|
|
71
|
+
priority: {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: ["P1", "P2", "P3", "P4"],
|
|
74
|
+
description: "Filter by priority",
|
|
75
|
+
},
|
|
76
|
+
limit: {
|
|
77
|
+
type: "number",
|
|
78
|
+
description: "Maximum number of tasks to return",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "task_get",
|
|
85
|
+
description: "Get details of a specific task by ID",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
id: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "Task ID (UUID or uid like A1, A2)",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
required: ["id"],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "task_update",
|
|
99
|
+
description: "Update an existing task",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
id: {
|
|
104
|
+
type: "string",
|
|
105
|
+
description: "Task ID to update",
|
|
106
|
+
},
|
|
107
|
+
title: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "New title",
|
|
110
|
+
},
|
|
111
|
+
body: {
|
|
112
|
+
type: "string",
|
|
113
|
+
description: "New description",
|
|
114
|
+
},
|
|
115
|
+
priority: {
|
|
116
|
+
type: "string",
|
|
117
|
+
enum: ["P1", "P2", "P3", "P4"],
|
|
118
|
+
description: "New priority",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
required: ["id"],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "task_complete",
|
|
126
|
+
description: "Mark a task as completed",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
id: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "Task ID to complete",
|
|
133
|
+
},
|
|
134
|
+
summary: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "Optional completion summary",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
required: ["id"],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "task_comment",
|
|
144
|
+
description: "Add a comment/update to a task",
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
id: {
|
|
149
|
+
type: "string",
|
|
150
|
+
description: "Task ID to comment on",
|
|
151
|
+
},
|
|
152
|
+
message: {
|
|
153
|
+
type: "string",
|
|
154
|
+
description: "Comment text (supports markdown)",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
required: ["id", "message"],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Handle tool calls
|
|
165
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
166
|
+
const { name, arguments: args } = request.params;
|
|
167
|
+
|
|
168
|
+
// Check if configured
|
|
169
|
+
const token = config.get("token");
|
|
170
|
+
if (!token) {
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: "text",
|
|
175
|
+
text: "Error: 1Medium CLI not configured. Run '1m login' to set up authentication.",
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
isError: true,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
let result;
|
|
184
|
+
|
|
185
|
+
switch (name) {
|
|
186
|
+
case "task_create": {
|
|
187
|
+
const payload = {
|
|
188
|
+
title: args.title,
|
|
189
|
+
body_md: args.body,
|
|
190
|
+
priority: args.priority || "P2",
|
|
191
|
+
source: args.source || "claude-code",
|
|
192
|
+
context: args.context || {},
|
|
193
|
+
};
|
|
194
|
+
result = await api.createTask(payload);
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: `Task created:\n ID: ${result.task.id}\n Title: ${result.task.title}\n Priority: ${result.task.priority}`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case "task_list": {
|
|
206
|
+
const params = {};
|
|
207
|
+
if (args.status) params.status = args.status;
|
|
208
|
+
if (args.priority) params.priority = args.priority;
|
|
209
|
+
if (args.limit) params.limit = args.limit;
|
|
210
|
+
|
|
211
|
+
result = await api.listTasks(params);
|
|
212
|
+
const tasks = result.tasks || [];
|
|
213
|
+
|
|
214
|
+
if (tasks.length === 0) {
|
|
215
|
+
return {
|
|
216
|
+
content: [{ type: "text", text: "No tasks found." }],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const taskList = tasks
|
|
221
|
+
.map((t) => {
|
|
222
|
+
const check = t.completedAt ? "[x]" : "[ ]";
|
|
223
|
+
return `${check} ${t.priority} ${t.title}\n ID: ${t.id}`;
|
|
224
|
+
})
|
|
225
|
+
.join("\n\n");
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
content: [
|
|
229
|
+
{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: `Tasks (${result.total} total):\n\n${taskList}`,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
case "task_get": {
|
|
238
|
+
result = await api.getTask(args.id);
|
|
239
|
+
const t = result.task;
|
|
240
|
+
const status = t.completedAt ? "completed" : "open";
|
|
241
|
+
|
|
242
|
+
let text = `${t.title}\n\n ID: ${t.id}\n Status: ${status}\n Priority: ${t.priority}`;
|
|
243
|
+
|
|
244
|
+
if (t.body_md) {
|
|
245
|
+
text += `\n\n Description:\n${t.body_md}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (t.comments && t.comments.length > 0) {
|
|
249
|
+
text += `\n\n Comments:`;
|
|
250
|
+
for (const c of t.comments) {
|
|
251
|
+
text += `\n [${c.createdAt}] ${c.body_md}`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
content: [{ type: "text", text }],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
case "task_update": {
|
|
261
|
+
const payload = {};
|
|
262
|
+
if (args.title) payload.title = args.title;
|
|
263
|
+
if (args.body) payload.body_md = args.body;
|
|
264
|
+
if (args.priority) payload.priority = args.priority;
|
|
265
|
+
|
|
266
|
+
result = await api.updateTask(args.id, payload);
|
|
267
|
+
return {
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: "text",
|
|
271
|
+
text: `Task updated:\n ID: ${result.task.id}\n Title: ${result.task.title}\n Priority: ${result.task.priority}`,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case "task_complete": {
|
|
278
|
+
const payload = {};
|
|
279
|
+
if (args.summary) payload.completion_summary = args.summary;
|
|
280
|
+
|
|
281
|
+
result = await api.completeTask(args.id, payload);
|
|
282
|
+
return {
|
|
283
|
+
content: [
|
|
284
|
+
{
|
|
285
|
+
type: "text",
|
|
286
|
+
text: `Task completed!\n ID: ${result.task.id}\n Title: ${result.task.title}`,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
case "task_comment": {
|
|
293
|
+
result = await api.addComment(args.id, { body_md: args.message });
|
|
294
|
+
return {
|
|
295
|
+
content: [
|
|
296
|
+
{
|
|
297
|
+
type: "text",
|
|
298
|
+
text: `Comment added to task ${args.id}`,
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
default:
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
307
|
+
isError: true,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
return {
|
|
312
|
+
content: [
|
|
313
|
+
{
|
|
314
|
+
type: "text",
|
|
315
|
+
text: `Error: ${error.message}${error.details ? `\nDetails: ${JSON.stringify(error.details)}` : ""}`,
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
isError: true,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Start server
|
|
324
|
+
async function main() {
|
|
325
|
+
const transport = new StdioServerTransport();
|
|
326
|
+
await server.connect(transport);
|
|
327
|
+
console.error("1Medium MCP server running");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
main().catch(console.error);
|