@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 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);