@martenhq/cli 0.1.0 → 0.1.1
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/CHANGELOG.md +23 -0
- package/README.md +23 -6
- package/dist/marten.js +117 -43
- package/package.json +16 -26
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
- `marten login --scope` adds to the default scopes (`martens`, `tasks`, `registry`) instead of replacing them, and
|
|
6
|
+
signing in again as the same person keeps what this machine's key could already do; `--reset-scopes` narrows it
|
|
7
|
+
back to the defaults. Another person signing in on the machine starts from the defaults.
|
|
8
|
+
- Signing in doesn't wait on the previous key's server: asking it the old key's scopes and revoking it give up after
|
|
9
|
+
5 seconds.
|
|
10
|
+
- Signing in again writes the new key first, then revokes the old one; a revoke that fails is a warning, not a
|
|
11
|
+
failed login. When the key can't be made (`too_many_keys`), the sign-in's session is ended anyway.
|
|
12
|
+
- A key `marten login` stored is only sent to the server it was made on: `MARTEN_API_URL` applies with
|
|
13
|
+
`MARTEN_API_KEY`, or when signing in.
|
|
14
|
+
- The credentials file is written to a new file and renamed into place (never through a link at its path), 0600, in
|
|
15
|
+
a folder that is set to 0700 even if it was there before. On Windows it's written as is.
|
|
16
|
+
- `marten mcp` answers `null`, non-object lines and batches with Invalid Request (-32600), and a line that fails can
|
|
17
|
+
no longer stop the server.
|
|
18
|
+
- Task statuses are `todo`, `in_progress` and `done` (`needs_review` is gone).
|
|
19
|
+
|
|
20
|
+
## 0.1.0
|
|
21
|
+
|
|
22
|
+
- First release: `login`/`logout`/`whoami`, `martens`, `ask`, `chats`, `chat read`/`send`, `task list`/`add`/
|
|
23
|
+
`update`, `registry list`/`show`/`install`, `mcp`, `setup claude|codex`, `--json` everywhere.
|
package/README.md
CHANGED
|
@@ -22,17 +22,34 @@ marten login
|
|
|
22
22
|
|
|
23
23
|
It asks for your Marten email and sends the same sign-in email as the apps: type the 6-digit code here, or open the
|
|
24
24
|
link on any device. Then it keeps an API key named after this machine ("Nina's MacBook · CLI") in
|
|
25
|
-
`~/.config/marten/credentials` (readable only by you
|
|
25
|
+
`~/.config/marten/credentials` (readable only by you; on Windows, by your user profile's permissions). `marten
|
|
26
|
+
logout` revokes it; signing in again replaces it and revokes the old one.
|
|
26
27
|
|
|
27
|
-
A key has scopes
|
|
28
|
-
|
|
28
|
+
A key has scopes:
|
|
29
|
+
|
|
30
|
+
| Scope | Lets it |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `martens` (default) | list your Martens, read and post in their 1:1s with you, `ask` them |
|
|
33
|
+
| `tasks` (default) | tasks in your Martens' 1:1s (with `chats:read`, groups' tasks too) |
|
|
34
|
+
| `registry` (default) | browse the registry and install Martens for you |
|
|
35
|
+
| `chats:read` | read your groups and their tasks |
|
|
36
|
+
| `chats:write` | post as you in your groups, add or change tasks there, add Martens to them |
|
|
37
|
+
|
|
38
|
+
`--scope` adds to the defaults, and signing in again as the same person keeps what this machine's key could already
|
|
39
|
+
do. `--reset-scopes` starts from the defaults (plus any `--scope`), to narrow a key:
|
|
29
40
|
|
|
30
41
|
```sh
|
|
31
|
-
marten login --scope
|
|
42
|
+
marten login --scope chats:read --scope chats:write
|
|
43
|
+
marten login --reset-scopes
|
|
32
44
|
```
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
Scopes limit what the key does, not what your Martens do: a key with `martens` talks to your own Martens, and they act
|
|
47
|
+
for you as they do when you ask them in the apps (they can read and post in groups you share with them, and use the
|
|
48
|
+
apps you've connected for them).
|
|
49
|
+
|
|
50
|
+
In CI or for another agent, set `MARTEN_API_KEY` instead, with `MARTEN_API_URL` for a server other than
|
|
51
|
+
https://api.marten.sh. A key `marten login` stored is only ever sent to the server it was made on (`MARTEN_API_URL`
|
|
52
|
+
applies to it only when signing in).
|
|
36
53
|
|
|
37
54
|
## Commands
|
|
38
55
|
|
package/dist/marten.js
CHANGED
|
@@ -80,16 +80,20 @@ Usage:
|
|
|
80
80
|
a Marten assignee is woken then to remind them
|
|
81
81
|
--repeat <rule> "every weekday at 9", "every 2 weeks on friday", daily, or an RRULE (at most hourly)
|
|
82
82
|
--tz <zone> IANA time zone for --due and --repeat (default UTC)
|
|
83
|
+
--label <name> tag it (repeat for several): a short word people filter and group tasks by,
|
|
84
|
+
like "travel", "bills" or the app it came from ("Gmail")
|
|
83
85
|
marten task list [options] List the chat's tasks
|
|
84
86
|
--assignee <who> me (you, this Marten), a person or Marten here by name or id,
|
|
85
87
|
or none (unassigned)
|
|
86
88
|
--for-owner assigned to your owner (a person's Marten only)
|
|
87
|
-
--status <s> todo, in_progress,
|
|
89
|
+
--status <s> todo, in_progress, done or open (not done)
|
|
88
90
|
--due <d> today, overdue, upcoming or none
|
|
91
|
+
--label <name> tagged with it
|
|
89
92
|
marten task show <id> A task, with its activity
|
|
90
93
|
marten task update <id> [options] Change a task: --title, --description, --status,
|
|
91
94
|
--assignee (none to clear), --due (none to clear),
|
|
92
|
-
--remind, --repeat (none to stop), --tz
|
|
95
|
+
--remind, --repeat (none to stop), --tz,
|
|
96
|
+
--label <name> to tag it, --unlabel <name> to untag it
|
|
93
97
|
marten task start|done|reopen <id> Start, finish or reopen a task
|
|
94
98
|
marten task comment <id> <text> Log progress or a note in the task's activity
|
|
95
99
|
marten task delete <id> [--series] Delete a task (--series: stop its repeat too)
|
|
@@ -189,7 +193,8 @@ var OPTIONS = {
|
|
|
189
193
|
series: { type: "boolean" },
|
|
190
194
|
app: { type: "string" },
|
|
191
195
|
title: { type: "string" },
|
|
192
|
-
label: { type: "string" },
|
|
196
|
+
label: { type: "string", multiple: true },
|
|
197
|
+
unlabel: { type: "string", multiple: true },
|
|
193
198
|
message: { type: "string" },
|
|
194
199
|
version: { type: "string" },
|
|
195
200
|
width: { type: "string" },
|
|
@@ -282,7 +287,11 @@ function parseCommand(argv, now = new Date) {
|
|
|
282
287
|
throw new UsageError(`Unknown command: ${first}`);
|
|
283
288
|
}
|
|
284
289
|
}
|
|
285
|
-
var STATUSES = ["todo", "in_progress", "
|
|
290
|
+
var STATUSES = ["todo", "in_progress", "done"];
|
|
291
|
+
function labelsOf(given) {
|
|
292
|
+
const labels = given?.map((l) => l.trim()).filter(Boolean);
|
|
293
|
+
return labels?.length ? labels : undefined;
|
|
294
|
+
}
|
|
286
295
|
var DUE_FILTERS = ["today", "overdue", "upcoming", "none"];
|
|
287
296
|
function oneOf(value, allowed, option) {
|
|
288
297
|
if (value === undefined)
|
|
@@ -309,16 +318,26 @@ function parseTask(sub, rest, values, group) {
|
|
|
309
318
|
return rest[0];
|
|
310
319
|
};
|
|
311
320
|
switch (sub) {
|
|
312
|
-
case "add":
|
|
313
|
-
|
|
321
|
+
case "add": {
|
|
322
|
+
const labels = labelsOf(values.label);
|
|
323
|
+
return { name: "task-add", title: words(rest, "task title"), ...taskFields(values), ...labels ? { labels } : {}, group };
|
|
324
|
+
}
|
|
314
325
|
case "update": {
|
|
315
326
|
const taskId = id("marten task update <id> [options]");
|
|
316
327
|
if (values.title !== undefined && !values.title.trim())
|
|
317
328
|
throw new UsageError("--title can't be empty");
|
|
318
329
|
const status = oneOf(values.status, STATUSES, "--status");
|
|
319
|
-
const
|
|
330
|
+
const addLabels = labelsOf(values.label);
|
|
331
|
+
const removeLabels = labelsOf(values.unlabel);
|
|
332
|
+
const changes = {
|
|
333
|
+
...taskFields(values),
|
|
334
|
+
...values.title ? { title: values.title.trim() } : {},
|
|
335
|
+
...status ? { status } : {},
|
|
336
|
+
...addLabels ? { addLabels } : {},
|
|
337
|
+
...removeLabels ? { removeLabels } : {}
|
|
338
|
+
};
|
|
320
339
|
if (!Object.keys(changes).length)
|
|
321
|
-
throw new UsageError("Nothing to change: pass --title, --description, --status, --assignee, --due, --remind or --
|
|
340
|
+
throw new UsageError("Nothing to change: pass --title, --description, --status, --assignee, --due, --remind, --repeat, --label or --unlabel");
|
|
322
341
|
return { name: "task-update", id: taskId, ...changes, group };
|
|
323
342
|
}
|
|
324
343
|
case "list": {
|
|
@@ -326,7 +345,16 @@ function parseTask(sub, rest, values, group) {
|
|
|
326
345
|
const forOwner = values["for-owner"] === true;
|
|
327
346
|
if (assignee && forOwner)
|
|
328
347
|
throw new UsageError("Pass --assignee or --for-owner, not both");
|
|
329
|
-
|
|
348
|
+
const label = labelsOf(values.label)?.at(-1);
|
|
349
|
+
return {
|
|
350
|
+
name: "task-list",
|
|
351
|
+
assignee,
|
|
352
|
+
forOwner,
|
|
353
|
+
status: oneOf(values.status, [...STATUSES, "open"], "--status"),
|
|
354
|
+
due: oneOf(values.due, DUE_FILTERS, "--due"),
|
|
355
|
+
...label ? { label } : {},
|
|
356
|
+
group
|
|
357
|
+
};
|
|
330
358
|
}
|
|
331
359
|
case "show":
|
|
332
360
|
return { name: "task-show", id: id("marten task show <id>"), group };
|
|
@@ -354,7 +382,7 @@ function parseApp(sub, rest, values, group) {
|
|
|
354
382
|
throw new UsageError("Missing what to publish: marten sketch publish <file.jsx|file.html|folder>");
|
|
355
383
|
if (values.title !== undefined && !values.title.trim())
|
|
356
384
|
throw new UsageError("--title can't be empty");
|
|
357
|
-
return { name: "app-publish", path: rest[0], app: values.app, title: values.title?.trim(), label: values.label?.trim(), group };
|
|
385
|
+
return { name: "app-publish", path: rest[0], app: values.app, title: values.title?.trim(), label: values.label?.at(-1)?.trim(), group };
|
|
358
386
|
}
|
|
359
387
|
case "list":
|
|
360
388
|
return { name: "app-list", group };
|
|
@@ -809,10 +837,11 @@ function withQuery(path, group, params = {}) {
|
|
|
809
837
|
const text = query.toString();
|
|
810
838
|
return text ? `${path}?${text}` : path;
|
|
811
839
|
}
|
|
812
|
-
var STATUS_MARK = { todo: "[ ]", in_progress: "[~]",
|
|
840
|
+
var STATUS_MARK = { todo: "[ ]", in_progress: "[~]", done: "[x]" };
|
|
813
841
|
function formatTask(t) {
|
|
814
842
|
const time = t.scheduledAt && `${t.remind ? "remind" : "at"} ${when(t.scheduledAt)} UTC`;
|
|
815
|
-
const
|
|
843
|
+
const labels = (t.labels ?? []).map((l) => `#${l}`);
|
|
844
|
+
const details = [t.due && `due ${t.due}`, time, t.repeat && t.repeat.toLowerCase(), t.assignee && `@${t.assignee}`, ...labels].filter(Boolean);
|
|
816
845
|
return `${t.id} ${STATUS_MARK[t.status]} ${t.title}${details.length ? ` (${details.join(", ")})` : ""}`;
|
|
817
846
|
}
|
|
818
847
|
var SHOWN_OPTIONS = 25;
|
|
@@ -892,7 +921,7 @@ async function runCommand(command, api, stdin = readStdin, agentDir = AGENT_DIR)
|
|
|
892
921
|
return `${command.verb === "start" ? "Started" : command.verb === "done" ? "Done" : "Reopened"} ${formatTask(res.task)}`;
|
|
893
922
|
}
|
|
894
923
|
case "task-list": {
|
|
895
|
-
const query = { assignee: command.assignee, forOwner: command.forOwner ? "1" : undefined, status: command.status, due: command.due };
|
|
924
|
+
const query = { assignee: command.assignee, forOwner: command.forOwner ? "1" : undefined, status: command.status, due: command.due, label: command.label };
|
|
896
925
|
const { tasks } = await api("GET", withQuery("/cli/tasks", command.group, query));
|
|
897
926
|
return tasks.map(formatTask).join(`
|
|
898
927
|
`) || "No tasks.";
|
|
@@ -1275,7 +1304,8 @@ function pick(list, wanted, name, what) {
|
|
|
1275
1304
|
throw new MartenError(404, "not_found", `No ${what} called "${wanted}"${list.length ? ` (yours: ${list.map(name).join(", ")})` : ""}`);
|
|
1276
1305
|
}
|
|
1277
1306
|
// src/credentials/credentials.ts
|
|
1278
|
-
import {
|
|
1307
|
+
import { randomBytes } from "node:crypto";
|
|
1308
|
+
import { chmod, mkdir as mkdir5, readFile as readFile4, rename, rm, writeFile as writeFile5 } from "node:fs/promises";
|
|
1279
1309
|
import { dirname as dirname4, join as join5 } from "node:path";
|
|
1280
1310
|
var DEFAULT_API_URL = "https://api.marten.sh";
|
|
1281
1311
|
function credentialsPath(env2, home) {
|
|
@@ -1292,20 +1322,32 @@ async function readCredentials(path) {
|
|
|
1292
1322
|
return null;
|
|
1293
1323
|
}
|
|
1294
1324
|
}
|
|
1295
|
-
async function writeCredentials(path, credentials) {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
await
|
|
1325
|
+
async function writeCredentials(path, credentials, platform = process.platform) {
|
|
1326
|
+
const dir = dirname4(path);
|
|
1327
|
+
const text = `${JSON.stringify(credentials, null, 2)}
|
|
1328
|
+
`;
|
|
1329
|
+
await mkdir5(dir, { recursive: true, mode: 448 });
|
|
1330
|
+
if (platform === "win32") {
|
|
1331
|
+
await writeFile5(path, text);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
await chmod(dir, 448);
|
|
1335
|
+
const temp = join5(dir, `.credentials-${randomBytes(6).toString("hex")}.tmp`);
|
|
1336
|
+
try {
|
|
1337
|
+
await writeFile5(temp, text, { mode: 384, flag: "wx" });
|
|
1338
|
+
await rename(temp, path);
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
await rm(temp, { force: true });
|
|
1341
|
+
throw err;
|
|
1342
|
+
}
|
|
1300
1343
|
}
|
|
1301
1344
|
async function removeCredentials(path) {
|
|
1302
1345
|
await rm(path, { force: true });
|
|
1303
1346
|
}
|
|
1304
1347
|
function resolveAuth(env2, stored) {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
return { apiKey, apiUrl: env2.MARTEN_API_URL || stored?.apiUrl || DEFAULT_API_URL };
|
|
1348
|
+
if (env2.MARTEN_API_KEY)
|
|
1349
|
+
return { apiKey: env2.MARTEN_API_KEY, apiUrl: env2.MARTEN_API_URL || DEFAULT_API_URL };
|
|
1350
|
+
return stored ? { apiKey: stored.apiKey, apiUrl: stored.apiUrl } : null;
|
|
1309
1351
|
}
|
|
1310
1352
|
|
|
1311
1353
|
// src/login/login.ts
|
|
@@ -1377,12 +1419,17 @@ async function login(opts) {
|
|
|
1377
1419
|
stop.abort();
|
|
1378
1420
|
}
|
|
1379
1421
|
const keyName = `${opts.machine} · CLI`;
|
|
1380
|
-
|
|
1422
|
+
let made;
|
|
1423
|
+
try {
|
|
1424
|
+
const scopes = typeof opts.scopes === "function" ? opts.scopes(session.user.email) : opts.scopes;
|
|
1425
|
+
made = await call(doFetch, `${base}/api-keys`, { name: keyName, ...scopes?.length ? { scopes } : {} }, session.sessionToken);
|
|
1426
|
+
} finally {
|
|
1427
|
+
await call(doFetch, `${base}/auth/logout`, undefined, session.sessionToken).catch(() => {
|
|
1428
|
+
return;
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1381
1431
|
if (made.status !== 201)
|
|
1382
1432
|
throw refused(made.status, made.data);
|
|
1383
|
-
await call(doFetch, `${base}/auth/logout`, undefined, session.sessionToken).catch(() => {
|
|
1384
|
-
return;
|
|
1385
|
-
});
|
|
1386
1433
|
return { apiUrl: base, apiKey: made.data.token, keyId: made.data.key.id, keyName: made.data.key.name, email: session.user.email };
|
|
1387
1434
|
}
|
|
1388
1435
|
|
|
@@ -1434,7 +1481,7 @@ var TOOLS = [
|
|
|
1434
1481
|
{
|
|
1435
1482
|
name: "marten_list_tasks",
|
|
1436
1483
|
description: "List tasks in the person's chats (open ones and those done in the last 30 days).",
|
|
1437
|
-
inputSchema: { type: "object", properties: { chat: { type: "string" }, status: { type: "string", enum: ["todo", "in_progress", "
|
|
1484
|
+
inputSchema: { type: "object", properties: { chat: { type: "string" }, status: { type: "string", enum: ["todo", "in_progress", "done", "open"] } } },
|
|
1438
1485
|
command: (a) => ({ name: "task-list", ...defined({ chat: str(a, "chat"), status: str(a, "status") }) })
|
|
1439
1486
|
},
|
|
1440
1487
|
{
|
|
@@ -1446,7 +1493,7 @@ var TOOLS = [
|
|
|
1446
1493
|
{
|
|
1447
1494
|
name: "marten_update_task",
|
|
1448
1495
|
description: "Change a task: its title, status, assignee, time or repeat.",
|
|
1449
|
-
inputSchema: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, status: { type: "string", enum: ["todo", "in_progress", "
|
|
1496
|
+
inputSchema: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, status: { type: "string", enum: ["todo", "in_progress", "done"] }, ...taskProps }, required: ["id"] },
|
|
1450
1497
|
command: (a) => ({ name: "task-update", id: need(a, "id"), ...defined({ title: str(a, "title"), status: str(a, "status") }), ...taskOptions(a) })
|
|
1451
1498
|
},
|
|
1452
1499
|
{ name: "marten_registry_list", description: "List the Martens anyone can install from the registry.", inputSchema: { type: "object", properties: {} }, command: () => ({ name: "registry-list" }) },
|
|
@@ -1466,12 +1513,17 @@ var TOOLS = [
|
|
|
1466
1513
|
var reply = (id, result) => ({ jsonrpc: "2.0", id: id ?? null, result });
|
|
1467
1514
|
var fail = (id, code, message) => ({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
|
|
1468
1515
|
async function handleMcp(raw, run, serverVersion) {
|
|
1469
|
-
let
|
|
1516
|
+
let parsed;
|
|
1470
1517
|
try {
|
|
1471
|
-
|
|
1518
|
+
parsed = JSON.parse(raw);
|
|
1472
1519
|
} catch {
|
|
1473
1520
|
return fail(null, -32700, "Parse error");
|
|
1474
1521
|
}
|
|
1522
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || typeof parsed.method !== "string") {
|
|
1523
|
+
const id = parsed?.id;
|
|
1524
|
+
return fail(typeof id === "string" || typeof id === "number" ? id : null, -32600, "Invalid Request");
|
|
1525
|
+
}
|
|
1526
|
+
const request = parsed;
|
|
1475
1527
|
const isNotification = request.id === undefined;
|
|
1476
1528
|
switch (request.method) {
|
|
1477
1529
|
case "initialize":
|
|
@@ -1505,9 +1557,11 @@ async function serveMcp(lines, write, run, serverVersion) {
|
|
|
1505
1557
|
for await (const line of lines) {
|
|
1506
1558
|
if (!line.trim())
|
|
1507
1559
|
continue;
|
|
1508
|
-
const answering = handleMcp(line, run, serverVersion).then((answer) => {
|
|
1560
|
+
const answering = handleMcp(line, run, serverVersion).catch((err) => fail(null, -32603, `Internal error: ${err?.message ?? String(err)}`)).then((answer) => {
|
|
1509
1561
|
if (answer)
|
|
1510
1562
|
write(JSON.stringify(answer));
|
|
1563
|
+
}).catch(() => {
|
|
1564
|
+
return;
|
|
1511
1565
|
});
|
|
1512
1566
|
inFlight.add(answering);
|
|
1513
1567
|
answering.finally(() => inFlight.delete(answering));
|
|
@@ -1519,7 +1573,7 @@ async function serveMcp(lines, write, run, serverVersion) {
|
|
|
1519
1573
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
1520
1574
|
var SCOPES = ["martens", "chats:read", "chats:write", "tasks", "registry"];
|
|
1521
1575
|
var TASK_USAGE = ` marten task list [--chat <chat>] [--status <s>] [--tz <zone>]
|
|
1522
|
-
Your chats' tasks (--status todo, in_progress,
|
|
1576
|
+
Your chats' tasks (--status todo, in_progress, done or open);
|
|
1523
1577
|
times in --tz (default: this machine's zone)
|
|
1524
1578
|
marten task add <title> [--chat <chat>] [options]
|
|
1525
1579
|
Add a task (--chat may be left out when you have one 1:1)
|
|
@@ -1534,7 +1588,7 @@ var REGISTRY_USAGE = ` marten registry list Martens anyone c
|
|
|
1534
1588
|
var OUTSIDE_HELP = `marten — talk to your Martens from your terminal and your other agents
|
|
1535
1589
|
|
|
1536
1590
|
Usage:
|
|
1537
|
-
marten login [--email <you@…>] [--scope <scope>…]
|
|
1591
|
+
marten login [--email <you@…>] [--scope <scope>…] [--reset-scopes]
|
|
1538
1592
|
Sign in (an email with a link and a 6-digit code); keeps an API key for
|
|
1539
1593
|
this machine in ~/.config/marten/credentials. Scopes: martens, tasks and
|
|
1540
1594
|
registry by default; add chats:read and chats:write for your groups
|
|
@@ -1562,6 +1616,7 @@ var OPTIONS2 = {
|
|
|
1562
1616
|
version: { type: "boolean", short: "v" },
|
|
1563
1617
|
email: { type: "string" },
|
|
1564
1618
|
scope: { type: "string", multiple: true },
|
|
1619
|
+
"reset-scopes": { type: "boolean" },
|
|
1565
1620
|
timeout: { type: "string" },
|
|
1566
1621
|
limit: { type: "string" },
|
|
1567
1622
|
chat: { type: "string" },
|
|
@@ -1575,7 +1630,7 @@ var OPTIONS2 = {
|
|
|
1575
1630
|
description: { type: "string" },
|
|
1576
1631
|
remind: { type: "boolean" }
|
|
1577
1632
|
};
|
|
1578
|
-
var STATUSES2 = ["todo", "in_progress", "
|
|
1633
|
+
var STATUSES2 = ["todo", "in_progress", "done"];
|
|
1579
1634
|
var MAX_ASK_SECONDS = 120;
|
|
1580
1635
|
function argsProblem(err) {
|
|
1581
1636
|
const message = err.message;
|
|
@@ -1626,12 +1681,17 @@ function parseOutside(argv) {
|
|
|
1626
1681
|
return done({ name: "help" });
|
|
1627
1682
|
switch (first) {
|
|
1628
1683
|
case "login": {
|
|
1629
|
-
none([second], "marten login [--email <address>] [--scope <scope>…]");
|
|
1684
|
+
none([second], "marten login [--email <address>] [--scope <scope>…] [--reset-scopes]");
|
|
1630
1685
|
const scopes = values.scope?.map((s) => s.trim()).filter(Boolean);
|
|
1631
1686
|
const unknown = scopes?.find((s) => !SCOPES.includes(s));
|
|
1632
1687
|
if (unknown)
|
|
1633
1688
|
throw new UsageError(`Unknown scope ${unknown}: use ${SCOPES.join(", ")}`);
|
|
1634
|
-
return done({
|
|
1689
|
+
return done({
|
|
1690
|
+
name: "login",
|
|
1691
|
+
...values.email?.trim() ? { email: values.email.trim() } : {},
|
|
1692
|
+
...scopes?.length ? { scopes } : {},
|
|
1693
|
+
...values["reset-scopes"] ? { resetScopes: true } : {}
|
|
1694
|
+
});
|
|
1635
1695
|
}
|
|
1636
1696
|
case "logout":
|
|
1637
1697
|
case "whoami":
|
|
@@ -1997,10 +2057,13 @@ ${content}${next}`, data: { path: wrote, content } };
|
|
|
1997
2057
|
case "login": {
|
|
1998
2058
|
const previous = await readCredentials(path);
|
|
1999
2059
|
const apiUrl = io.env.MARTEN_API_URL || previous?.apiUrl || DEFAULT_API_URL;
|
|
2000
|
-
const
|
|
2001
|
-
|
|
2002
|
-
|
|
2060
|
+
const had = previous && !command.resetScopes ? await scopesOf(io, previous) : null;
|
|
2061
|
+
const scopes = (email) => [...new Set([...DEFAULT_SCOPES, ...had && had.email.toLowerCase() === email.toLowerCase() ? had.scopes : [], ...command.scopes ?? []])];
|
|
2062
|
+
const credentials = await login({ apiUrl, email: command.email, scopes, machine: io.machine, io, fetch: io.fetch });
|
|
2003
2063
|
await writeCredentials(path, credentials);
|
|
2064
|
+
if (previous && previous.apiKey !== credentials.apiKey) {
|
|
2065
|
+
await revoke(io, previous).catch((err) => io.stderr(`marten: signed in, but couldn't revoke the previous key "${previous.keyName}" (${err.message}); revoke it from another signed-in machine with marten logout, or ask support`));
|
|
2066
|
+
}
|
|
2004
2067
|
return { text: `Signed in as ${credentials.email}. This machine's key, "${credentials.keyName}", is in ${path}.`, data: { email: credentials.email, keyId: credentials.keyId, keyName: credentials.keyName, path } };
|
|
2005
2068
|
}
|
|
2006
2069
|
case "logout": {
|
|
@@ -2021,8 +2084,19 @@ ${content}${next}`, data: { path: wrote, content } };
|
|
|
2021
2084
|
return runApi(command, { marten: client(io, await readCredentials(path)), readStdin: io.readStdin, timeZone: io.timeZone });
|
|
2022
2085
|
}
|
|
2023
2086
|
}
|
|
2087
|
+
var DEFAULT_SCOPES = ["martens", "tasks", "registry"];
|
|
2088
|
+
var PREVIOUS_KEY_TIMEOUT_MS = 5000;
|
|
2089
|
+
var previousClient = (io, stored) => new Marten({
|
|
2090
|
+
apiKey: stored.apiKey,
|
|
2091
|
+
baseUrl: stored.apiUrl,
|
|
2092
|
+
fetch: (url, init) => (io.fetch ?? fetch)(url, { ...init, signal: AbortSignal.timeout(PREVIOUS_KEY_TIMEOUT_MS) })
|
|
2093
|
+
});
|
|
2094
|
+
async function scopesOf(io, stored) {
|
|
2095
|
+
const me = await previousClient(io, stored).me().catch(() => null);
|
|
2096
|
+
return me ? { email: me.user.email, scopes: me.key.scopes } : null;
|
|
2097
|
+
}
|
|
2024
2098
|
async function revoke(io, stored) {
|
|
2025
|
-
await
|
|
2099
|
+
await previousClient(io, stored).revokeKey().catch((err) => {
|
|
2026
2100
|
if (!(err instanceof MartenError && err.status === 401))
|
|
2027
2101
|
throw err;
|
|
2028
2102
|
});
|
|
@@ -2050,7 +2124,7 @@ function client(io, stored) {
|
|
|
2050
2124
|
// package.json
|
|
2051
2125
|
var package_default = {
|
|
2052
2126
|
name: "@martenhq/cli",
|
|
2053
|
-
version: "0.1.
|
|
2127
|
+
version: "0.1.1",
|
|
2054
2128
|
description: "marten: talk to your Martens from your terminal, scripts and other agents (Claude Code, Codex), and the CLI Martens use inside their boxes.",
|
|
2055
2129
|
license: "MIT",
|
|
2056
2130
|
type: "module",
|
|
@@ -2059,7 +2133,7 @@ var package_default = {
|
|
|
2059
2133
|
keywords: ["marten", "cli", "agents", "mcp"],
|
|
2060
2134
|
engines: { node: ">=20" },
|
|
2061
2135
|
bin: { marten: "dist/marten.js" },
|
|
2062
|
-
files: ["dist/marten.js", "README.md", "LICENSE"],
|
|
2136
|
+
files: ["dist/marten.js", "README.md", "CHANGELOG.md", "LICENSE"],
|
|
2063
2137
|
publishConfig: { access: "public" },
|
|
2064
2138
|
scripts: {
|
|
2065
2139
|
build: "bun build.ts",
|
package/package.json
CHANGED
|
@@ -1,33 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@martenhq/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "marten: talk to your Martens from your terminal, scripts and other agents (Claude Code, Codex), and the CLI Martens use inside their boxes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://marten.sh",
|
|
8
|
-
"repository": {
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
},
|
|
24
|
-
"scripts": {
|
|
25
|
-
"build": "bun build.ts",
|
|
26
|
-
"test": "bun test",
|
|
27
|
-
"typecheck": "tsc -p tsconfig.json"
|
|
28
|
-
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"@martenhq/sdk": "0.1.0",
|
|
31
|
-
"@types/bun": "^1.4.2"
|
|
32
|
-
}
|
|
8
|
+
"repository": { "type": "git", "url": "git+https://github.com/RedPlanetHQ/marten-app.git", "directory": "packages/cli" },
|
|
9
|
+
"keywords": ["marten", "cli", "agents", "mcp"],
|
|
10
|
+
"engines": { "node": ">=20" },
|
|
11
|
+
"bin": { "marten": "dist/marten.js" },
|
|
12
|
+
"files": ["dist/marten.js", "README.md", "CHANGELOG.md", "LICENSE"],
|
|
13
|
+
"publishConfig": { "access": "public" },
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "bun build.ts",
|
|
16
|
+
"test": "bun test",
|
|
17
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@martenhq/sdk": "workspace:*",
|
|
21
|
+
"@types/bun": "^1.4.2"
|
|
22
|
+
}
|
|
33
23
|
}
|