Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Changelog

All notable changes to `getmnemo-cli` are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
[SemVer](https://semver.org/).

## [0.3.0] — 2026-09-02

### Added
- `getmnemo brief [--container <tag>] [--date] [--timezone] [--days] [--sections]` — your Daily Brief (overdue / due today / coming up, important dates, today's meetings, follow-ups, last-24h captures). `GET /v1/brief`, scope `brief:read`.
- `getmnemo timeline --container <tag> [--from --to --types --direction --limit --cursor]` — merged chronological stream for one container. `GET /v1/timeline`, scope `timeline:read`.
- `getmnemo people list|get <slug>|add <name>` — one memory container per person, with `--relationship --email --phone --company --notes --alias --important-date`. `/v1/people`, scopes `people:read` / `people:write`.
- `getmnemo reminders list|upcoming|add <content> --due <iso>|complete <id>` — reminders are memories with a due date; `add` targets `--person <slug>` or a container. `/v1/reminders`, scopes `reminders:read` / `reminders:write`.
- `getmnemo meetings upcoming|brief <documentId>` — calendar meetings with attendee ↔ person matching and a pre-meeting brief. `/v1/meetings`, scope `meetings:read` (+ `answer:read` for briefs).
- `getmnemo memories merge <id...> [--into <id>] [--content] [--type] [--merge-key] [-y]` — fold 2–20 memories into one; sources are soft-deleted and restorable. `POST /v1/memories/merge`, scopes `memories:write` + `memories:delete`.
- `--json` error envelope now carries the API's stable `code` (e.g. `FEATURE_DISABLED`, `PERSON_NOT_FOUND`) and `status`.

### Changed
- `--version` is read from `package.json` at runtime (the hardcoded const had drifted to 0.2.0 while the package was 0.2.1).
- All new commands validate arguments locally (bounded integers, ISO-8601 dates, enums, csv subsets) and exit `2` before any request is sent.

### Fixed
- `getmnemo mcp` now emits `npx -y getmnemo-mcp` (the published package) instead of the never-published `@mnemo/mcp`.

### Notes
- The new commands need the matching scopes on your API key — existing keys gain nothing by default; mint or edit a key in the dashboard.
- These features are dark-launched server-side; a `503 FEATURE_DISABLED` means the deployment has not enabled that feature yet.
- The new surfaces use a CLI-owned transport (`src/lib/personal-api.ts`) with wire shapes identical to `getmnemo@0.6.0`'s resources; the dependency stays `^0.5.1` until 0.6.0 is published.

## [0.2.1] — 2026-08-19

### Fixed
- Require a container scope on by-id memory routes (`get`, `rm`); bump `getmnemo` to `^0.5.1`.

## [0.2.0]

### Changed
- API-contract reconciliation (`q` search field, `results` hits, `items` receipts) + CI prod smoke gate before publish.
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ getmnemo doctor # verify auth + API reachability

`--container` / `-C` accepts a container tag (e.g. `user:jane`) and is required on every memory command. Resolution order: `--container` flag → `GETMNEMO_CONTAINER` env → `defaultContainerTag` in config.

### Daily brief, timeline, people, reminders, meetings

These commands need the matching scopes on your API key (`brief:read`, `timeline:read`, `people:read|write`, `reminders:read|write`, `meetings:read`, and `memories:write`+`memories:delete` for merge). A `503 FEATURE_DISABLED` means the deployment has not enabled that feature yet.

| Command | Description |
| --- | --- |
| `getmnemo brief [--container <tag>] [--date YYYY-MM-DD] [--timezone <tz>] [--days 7] [--sections core,followUps,meetings]` | Your Daily Brief: overdue / due today / coming up, important dates, today's meetings, follow-ups, last-24h captures. Container resolves like the memory commands; timezone defaults to your system zone. |
| `getmnemo timeline --container <tag> [--from <iso>] [--to <iso>] [--types memory,reminder,document,event] [--direction desc\|asc] [--limit 50] [--cursor <c>]` | Merged chronological stream of memories, reminders and documents for one container (`event` is opt-in). |
| `getmnemo people list [-q <text>] [--include-archived] [--limit 50] [--cursor <c>]` | List people (one memory container per person, `person:<slug>`). |
| `getmnemo people get <slug>` | Show a person with contact fields, important dates and reminder counts. |
| `getmnemo people add "<name>" [--slug] [--relationship] [--email] [--phone] [--company] [--notes] [--alias <a>...] [--important-date label=YYYY-MM-DD[:recurring]...]` | Add a person. A slug collision is a `409 PERSON_EXISTS`; retry with `--slug`. |
| `getmnemo reminders list [--status open\|completed\|all] [--days <n>] [--due-after <iso>] [--due-before <iso>] [-C <tag>] [--container-type person] [--limit 50] [--cursor <c>]` | Workspace-wide reminders ordered by due date. `-C` here is an explicit filter only (env/config defaults are not applied). |
| `getmnemo reminders upcoming [--days 7] [--timezone <tz>] [-C <tag>] [--container-type <t>] [--limit 50]` | Overdue / due today / coming up buckets in your timezone, plus important dates in the window. |
| `getmnemo reminders add "<content>" --due <iso> (--person <slug> \| --container <tag>) [--idempotency-key <k>] [-m key=value ...]` | Create a reminder for a person (lands in `person:<slug>`) or a container (flag → `GETMNEMO_CONTAINER` → config). |
| `getmnemo reminders complete <id>` | Mark a reminder done (clears `dueAt`, keeps the memory). |
| `getmnemo meetings upcoming [--days 7] [--limit 50] [--cursor <c>] [-C <tag>]` | Upcoming meetings from connected calendars, attendees matched to people. |
| `getmnemo meetings brief <documentId> [-q "<question>"]` | Pre-meeting brief: reader summary, each attendee's open reminders + recent memories, previous meetings. |
| `getmnemo memories merge <id> <id> [<id>...] [--into <id>] [--content "<text>"] [--type <memoryType>] [--merge-key <k>] [-m key=value ...] [-C <tag>] [-y]` | Merge 2–20 memories from one container. With `--into` that memory survives; otherwise `--content` is required and a new memory is created. Sources are soft-deleted (restorable). Prompts unless `-y`; refuses without `-y` in non-interactive shells. |

### Workspaces

| Command | Description |
Expand All @@ -65,7 +84,7 @@ getmnemo doctor # verify auth + API reachability

| Command | Description |
| --- | --- |
| `getmnemo mcp [--client claude\|cursor]` | Print an MCP server config snippet. |
| `getmnemo mcp [--client claude\|cursor]` | Print an MCP server config snippet (`npx -y getmnemo-mcp`). |
| `getmnemo doctor` | Check auth + `GET /health`. |

## Global flags
Expand All @@ -80,7 +99,9 @@ getmnemo doctor # verify auth + API reachability
| --- | --- |
| `0` | Success |
| `1` | Runtime error (auth missing, API failure, not found, ...) |
| `2` | Invalid arguments / unknown command |
| `2` | Invalid arguments / unknown command / missing container / confirmation required |

With `--json`, runtime errors are written to stderr as `{"ok": false, "error": "<message>", "code": "<API code>", "status": <http>}` — `code` is the API's stable error code (`FEATURE_DISABLED`, `PERSON_NOT_FOUND`, `MERGE_CROSS_CONTAINER`, ...), and argument errors as `{"ok": false, "error": "invalid_argument" | "container_required" | "confirmation_required", "message": "..."}`.

## Environment variables

Expand All @@ -107,6 +128,24 @@ getmnemo rm mem_01HX... --container org:acme --yes

# generate Claude Desktop MCP config
getmnemo mcp --client claude > claude_desktop_config.json

# morning routine
getmnemo brief --container user:me --timezone Asia/Karachi
getmnemo reminders upcoming --days 3

# people + reminders
getmnemo people add "Jane Doe" --relationship client --email jane@example.com --important-date birthday=1990-05-04:recurring
getmnemo reminders add "Send Jane the proposal" --due 2026-09-05T09:00:00Z --person jane-doe
getmnemo reminders complete <reminder-id>

# what happened in a container, newest first
getmnemo timeline --container person:jane-doe --types memory,reminder --limit 20

# prep for the next meeting
getmnemo meetings upcoming --days 1 --json | jq -r '.items[0].documentId' | xargs getmnemo meetings brief

# fold duplicate memories into one
getmnemo memories merge <id-a> <id-b> --into <id-a> --container user:me -y
```

## Development
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "getmnemo-cli",
"version": "0.2.1",
"version": "0.3.0",
"description": "Official CLI for Mnemo — manage memories, workspaces, and integrations from your terminal.",
"type": "module",
"bin": {
Expand Down
46 changes: 46 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,52 @@ describe("Mnemo CLI", () => {
expect(stdout).toMatch(/workspace/);
expect(stdout).toMatch(/mcp/);
expect(stdout).toMatch(/doctor/);
expect(stdout).toMatch(/brief/);
expect(stdout).toMatch(/timeline/);
expect(stdout).toMatch(/people/);
expect(stdout).toMatch(/reminders/);
expect(stdout).toMatch(/meetings/);
expect(stdout).toMatch(/memories/);
});

it("--version prints the package.json version", async () => {
const program = buildCli();
program.exitOverride();
try {
await program.parseAsync(["node", "getmnemo", "--version"]);
} catch {
// commander throws on version by design
}
expect(stdout.trim()).toBe("0.3.0");
});

it("exits 2 for an unknown option on a subcommand (exit callback is inherited)", async () => {
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
throw new Error("__exit__");
}) as never);
const program = buildCli();
await expect(program.parseAsync(["node", "getmnemo", "people", "list", "--bogus"])).rejects.toThrow("__exit__");
expect(exitSpy).toHaveBeenCalledWith(2);
expect(stderr).toMatch(/unknown option '--bogus'/);
});

it("lists subcommands in group help", async () => {
for (const [group, subs] of [
["people", ["list", "get", "add"]],
["reminders", ["list", "upcoming", "add", "complete"]],
["meetings", ["upcoming", "brief"]],
["memories", ["merge"]],
] as const) {
stdout = "";
const program = buildCli();
program.exitOverride();
try {
await program.parseAsync(["node", "getmnemo", group, "--help"]);
} catch {
// commander throws on help by design
}
for (const sub of subs) expect(stdout).toMatch(new RegExp(`^\\s+${sub}\\b`, "m"));
}
});

it("doctor reports failure when API is unreachable (mocked)", async () => {
Expand Down
58 changes: 46 additions & 12 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ import { registerMemoryCommands } from "./commands/memory.js";
import { registerWorkspaceCommands } from "./commands/workspace.js";
import { registerMcpCommand } from "./commands/mcp.js";
import { registerDoctorCommand } from "./commands/doctor.js";

const VERSION = "0.2.0";
import { registerBriefCommand } from "./commands/brief.js";
import { registerTimelineCommand } from "./commands/timeline.js";
import { registerPeopleCommands } from "./commands/people.js";
import { registerRemindersCommands } from "./commands/reminders.js";
import { registerMeetingsCommands } from "./commands/meetings.js";
import { registerMemoriesCommands } from "./commands/memories.js";
import { PersonalApiError } from "./lib/personal-api.js";
import { CLI_VERSION } from "./lib/version.js";

// Color detection: kleur's autodetect can produce ANSI escapes when this CLI
// is spawned as a subprocess (e.g. by `claude`, CI runners, or scripts that
Expand Down Expand Up @@ -39,32 +45,54 @@ function configureColor(): void {
}
configureColor();

/** Commander error codes that mean "bad invocation" → exit 2. */
const USAGE_ERROR_CODES = new Set([
"commander.unknownCommand",
"commander.unknownOption",
"commander.missingArgument",
"commander.missingMandatoryOptionValue",
"commander.optionMissingArgument",
"commander.excessArguments",
"commander.invalidArgument",
"commander.conflictingOption",
]);

export function buildCli(): Command {
const program = new Command();

program
.name("getmnemo")
.description(kleur.cyan("Mnemo CLI") + " — manage memories from your terminal.")
.version(VERSION, "-v, --version", "print the CLI version")
.version(CLI_VERSION, "-v, --version", "print the CLI version")
.option("--json", "format output as JSON for machine consumption", false)
.showHelpAfterError("(add --help for additional information)");

registerAuthCommands(program);
registerMemoryCommands(program);
registerWorkspaceCommands(program);
registerMcpCommand(program);
registerDoctorCommand(program);

// Installed BEFORE the subcommands are registered: commander copies the
// exit callback into each `.command()` at creation time, so a late
// override only covers the root and `getmnemo people list --bogus` would
// exit 1 instead of the documented 2.
program.exitOverride((err) => {
if (err.code === "commander.helpDisplayed" || err.code === "commander.version") {
if (err.code === "commander.helpDisplayed" || err.code === "commander.version" || err.code === "commander.help") {
process.exit(0);
}
if (err.code === "commander.unknownCommand" || err.code === "commander.unknownOption") {
if (USAGE_ERROR_CODES.has(err.code)) {
process.exit(2);
}
process.exit(err.exitCode ?? 1);
});

registerAuthCommands(program);
registerMemoryCommands(program);
registerWorkspaceCommands(program);
registerMcpCommand(program);
registerDoctorCommand(program);
registerBriefCommand(program);
registerTimelineCommand(program);
registerPeopleCommands(program);
registerRemindersCommands(program);
registerMeetingsCommands(program);
registerMemoriesCommands(program);

return program;
}

Expand All @@ -82,7 +110,13 @@ async function main(): Promise<void> {
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (wantsJson) {
process.stderr.write(JSON.stringify({ ok: false, error: message }) + "\n");
// API errors carry the stable `code` from the API envelope (e.g.
// FEATURE_DISABLED, PERSON_NOT_FOUND) so scripts can branch on it.
const detail =
err instanceof PersonalApiError
? { ok: false, error: message, code: err.code, status: err.status }
: { ok: false, error: message };
process.stderr.write(JSON.stringify(detail) + "\n");
} else {
process.stderr.write(kleur.red(`error: ${message}\n`));
}
Expand Down
Loading