otter-axi 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/dist/bin/otter-axi.d.ts +2 -0
  4. package/dist/bin/otter-axi.js +4 -0
  5. package/dist/bin/otter-axi.js.map +1 -0
  6. package/dist/src/cli.d.ts +5 -0
  7. package/dist/src/cli.js +50 -0
  8. package/dist/src/cli.js.map +1 -0
  9. package/dist/src/commands/auth.d.ts +3 -0
  10. package/dist/src/commands/auth.js +144 -0
  11. package/dist/src/commands/auth.js.map +1 -0
  12. package/dist/src/commands/doctor.d.ts +3 -0
  13. package/dist/src/commands/doctor.js +47 -0
  14. package/dist/src/commands/doctor.js.map +1 -0
  15. package/dist/src/commands/fetch.d.ts +5 -0
  16. package/dist/src/commands/fetch.js +132 -0
  17. package/dist/src/commands/fetch.js.map +1 -0
  18. package/dist/src/commands/home.d.ts +7 -0
  19. package/dist/src/commands/home.js +28 -0
  20. package/dist/src/commands/home.js.map +1 -0
  21. package/dist/src/commands/search.d.ts +3 -0
  22. package/dist/src/commands/search.js +105 -0
  23. package/dist/src/commands/search.js.map +1 -0
  24. package/dist/src/commands/setup.d.ts +6 -0
  25. package/dist/src/commands/setup.js +25 -0
  26. package/dist/src/commands/setup.js.map +1 -0
  27. package/dist/src/config.d.ts +62 -0
  28. package/dist/src/config.js +89 -0
  29. package/dist/src/config.js.map +1 -0
  30. package/dist/src/dates.d.ts +5 -0
  31. package/dist/src/dates.js +36 -0
  32. package/dist/src/dates.js.map +1 -0
  33. package/dist/src/flags.d.ts +19 -0
  34. package/dist/src/flags.js +45 -0
  35. package/dist/src/flags.js.map +1 -0
  36. package/dist/src/meta.d.ts +3 -0
  37. package/dist/src/meta.js +22 -0
  38. package/dist/src/meta.js.map +1 -0
  39. package/dist/src/otter/client.d.ts +30 -0
  40. package/dist/src/otter/client.js +120 -0
  41. package/dist/src/otter/client.js.map +1 -0
  42. package/dist/src/otter/loopback.d.ts +13 -0
  43. package/dist/src/otter/loopback.js +68 -0
  44. package/dist/src/otter/loopback.js.map +1 -0
  45. package/dist/src/otter/oauth.d.ts +32 -0
  46. package/dist/src/otter/oauth.js +183 -0
  47. package/dist/src/otter/oauth.js.map +1 -0
  48. package/dist/src/output.d.ts +20 -0
  49. package/dist/src/output.js +44 -0
  50. package/dist/src/output.js.map +1 -0
  51. package/dist/src/transcript.d.ts +21 -0
  52. package/dist/src/transcript.js +61 -0
  53. package/dist/src/transcript.js.map +1 -0
  54. package/package.json +50 -0
  55. package/skills/otter-axi/SKILL.md +68 -0
@@ -0,0 +1,68 @@
1
+ import { createServer } from "node:http";
2
+ import { createServer as netServer } from "node:net";
3
+ import { AxiError } from "axi-sdk-js";
4
+ /** Return the first port from `candidates` that is bindable on 127.0.0.1. */
5
+ export async function pickPort(candidates) {
6
+ for (const port of candidates) {
7
+ const free = await new Promise((resolve) => {
8
+ const probe = netServer();
9
+ probe.once("error", () => resolve(false));
10
+ probe.listen(port, "127.0.0.1", () => probe.close(() => resolve(true)));
11
+ });
12
+ if (free)
13
+ return port;
14
+ }
15
+ throw new AxiError(`No free loopback port among ${candidates.join(", ")}`, "AUTH", ["Close whatever is using those ports and retry `otter-axi auth login`"]);
16
+ }
17
+ /**
18
+ * Start a loopback HTTP server on `127.0.0.1:port` that captures the OAuth redirect at
19
+ * `/callback`. The browser response is sent UTF-8 (so the em-dash renders correctly).
20
+ */
21
+ export async function startLoopback(port) {
22
+ let resolveCode;
23
+ let rejectCode;
24
+ const codePromise = new Promise((res, rej) => {
25
+ resolveCode = res;
26
+ rejectCode = rej;
27
+ });
28
+ const server = createServer((req, res) => {
29
+ const u = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
30
+ if (u.pathname !== "/callback") {
31
+ res.writeHead(404).end();
32
+ return;
33
+ }
34
+ const code = u.searchParams.get("code");
35
+ const err = u.searchParams.get("error");
36
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
37
+ res.end(`<!doctype html><html><head><meta charset="utf-8"><title>otter-axi</title></head>` +
38
+ `<body style="font-family:system-ui;padding:2.5rem;color:#222">` +
39
+ `<h2>otter-axi: ${code ? "authorized — you can close this tab" : `error: ${err ?? "unknown"}`}</h2>` +
40
+ `</body></html>`);
41
+ if (code)
42
+ resolveCode(code);
43
+ else
44
+ rejectCode(new AxiError(`Authorization was denied (${err ?? "unknown"})`, "AUTH", [
45
+ "Run `otter-axi auth login` to try again",
46
+ ]));
47
+ });
48
+ await new Promise((resolve, reject) => {
49
+ server.once("error", reject);
50
+ server.listen(port, "127.0.0.1", resolve);
51
+ });
52
+ return {
53
+ port,
54
+ waitForCode(timeoutMs) {
55
+ let timer;
56
+ const timeout = new Promise((_, rej) => {
57
+ timer = setTimeout(() => rej(new AxiError("Timed out waiting for authorization", "AUTH", [
58
+ "Run `otter-axi auth login` and approve promptly (codes are short-lived)",
59
+ ])), timeoutMs);
60
+ });
61
+ return Promise.race([codePromise, timeout]).finally(() => clearTimeout(timer));
62
+ },
63
+ close() {
64
+ server.close();
65
+ },
66
+ };
67
+ }
68
+ //# sourceMappingURL=loopback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback.js","sourceRoot":"","sources":["../../../src/otter/loopback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { AuthorizationServerMetadata, OAuthClientInformationMixed } from "@modelcontextprotocol/sdk/shared/auth.js";
2
+ import { type Config } from "../config.js";
3
+ export declare const MCP_URL = "https://mcp.otter.ai/mcp";
4
+ export declare const SCOPE = "profile:read conversations:read";
5
+ /** Candidate loopback ports (all registered as redirect_uris so any may be bound). */
6
+ export declare const LOOPBACK_PORTS: number[];
7
+ interface Discovery {
8
+ resource: string;
9
+ authServer: string;
10
+ asMetadata: AuthorizationServerMetadata;
11
+ }
12
+ /** Discover the protected-resource + authorization-server metadata. */
13
+ export declare function discover(): Promise<Discovery>;
14
+ /** Register a public client via DCR if one isn't already stored. Persists it to config. */
15
+ export declare function ensureClient(cfg: Config, disc: Discovery): Promise<{
16
+ cfg: Config;
17
+ client: OAuthClientInformationMixed;
18
+ }>;
19
+ /**
20
+ * Phase 1: discover, ensure a client, and build the PKCE authorize URL bound to `port`.
21
+ * Persists the verifier + redirect_uri so phase 2 (possibly a separate process) can finish.
22
+ */
23
+ export declare function prepareLogin(port: number): Promise<{
24
+ url: string;
25
+ }>;
26
+ /** Phase 2: exchange the authorization code for tokens using the persisted pending state. */
27
+ export declare function completeLogin(code: string): Promise<void>;
28
+ /** Refresh the stored access token using the refresh_token grant. Persists the result. */
29
+ export declare function refreshTokens(): Promise<void>;
30
+ /** Best-effort token revocation at the authorization server's revocation endpoint. */
31
+ export declare function revokeToken(): Promise<void>;
32
+ export {};
@@ -0,0 +1,183 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { discoverAuthorizationServerMetadata, discoverOAuthProtectedResourceMetadata, exchangeAuthorization, refreshAuthorization, registerClient, startAuthorization, } from "@modelcontextprotocol/sdk/client/auth.js";
3
+ import { clearPending, readConfig, readPending, writeConfig, writePending, } from "../config.js";
4
+ export const MCP_URL = "https://mcp.otter.ai/mcp";
5
+ export const SCOPE = "profile:read conversations:read";
6
+ /** Candidate loopback ports (all registered as redirect_uris so any may be bound). */
7
+ export const LOOPBACK_PORTS = [41789, 41790, 41791, 41792];
8
+ function redirectUris() {
9
+ return LOOPBACK_PORTS.map((p) => `http://127.0.0.1:${p}/callback`);
10
+ }
11
+ /** Discover the protected-resource + authorization-server metadata. */
12
+ export async function discover() {
13
+ const prm = await discoverOAuthProtectedResourceMetadata(MCP_URL);
14
+ const authServer = (prm.authorization_servers?.[0] ?? "https://otter.ai/").toString();
15
+ const asMetadata = await discoverAuthorizationServerMetadata(authServer);
16
+ if (!asMetadata) {
17
+ throw new AxiError("Could not discover Otter's OAuth authorization-server metadata", "AUTH", ["Check connectivity to https://otter.ai and retry"]);
18
+ }
19
+ return {
20
+ resource: (prm.resource ?? "https://mcp.otter.ai/").toString(),
21
+ authServer,
22
+ asMetadata,
23
+ };
24
+ }
25
+ /** Register a public client via DCR if one isn't already stored. Persists it to config. */
26
+ export async function ensureClient(cfg, disc) {
27
+ if (cfg.client?.client_id) {
28
+ return {
29
+ cfg,
30
+ client: {
31
+ client_id: cfg.client.client_id,
32
+ ...(cfg.client.client_secret
33
+ ? { client_secret: cfg.client.client_secret }
34
+ : {}),
35
+ },
36
+ };
37
+ }
38
+ const full = await registerClient(disc.authServer, {
39
+ metadata: disc.asMetadata,
40
+ scope: SCOPE,
41
+ clientMetadata: {
42
+ client_name: "otter-axi",
43
+ redirect_uris: redirectUris(),
44
+ grant_types: ["authorization_code", "refresh_token"],
45
+ response_types: ["code"],
46
+ token_endpoint_auth_method: "none",
47
+ scope: SCOPE,
48
+ },
49
+ });
50
+ const stored = {
51
+ client_id: full.client_id,
52
+ ...(full.client_secret ? { client_secret: full.client_secret } : {}),
53
+ registered_at: new Date().toISOString(),
54
+ };
55
+ const next = { ...cfg, client: stored };
56
+ writeConfig(next);
57
+ return { cfg: next, client: full };
58
+ }
59
+ /**
60
+ * Phase 1: discover, ensure a client, and build the PKCE authorize URL bound to `port`.
61
+ * Persists the verifier + redirect_uri so phase 2 (possibly a separate process) can finish.
62
+ */
63
+ export async function prepareLogin(port) {
64
+ const disc = await discover();
65
+ const { client } = await ensureClient(readConfig(), disc);
66
+ const redirect_uri = `http://127.0.0.1:${port}/callback`;
67
+ const { authorizationUrl, codeVerifier } = await startAuthorization(disc.authServer, {
68
+ metadata: disc.asMetadata,
69
+ clientInformation: client,
70
+ redirectUrl: redirect_uri,
71
+ scope: SCOPE,
72
+ resource: new URL(disc.resource),
73
+ });
74
+ writePending({
75
+ verifier: codeVerifier,
76
+ redirect_uri,
77
+ port,
78
+ resource: disc.resource,
79
+ auth_server: disc.authServer,
80
+ url: authorizationUrl.toString(),
81
+ expires_at: Date.now() + 10 * 60 * 1000,
82
+ });
83
+ return { url: authorizationUrl.toString() };
84
+ }
85
+ function tokensToConfig(t) {
86
+ return {
87
+ access_token: t.access_token,
88
+ refresh_token: t.refresh_token,
89
+ token_type: t.token_type,
90
+ scope: t.scope,
91
+ expires_at: typeof t.expires_in === "number"
92
+ ? Date.now() + t.expires_in * 1000
93
+ : undefined,
94
+ };
95
+ }
96
+ /** Phase 2: exchange the authorization code for tokens using the persisted pending state. */
97
+ export async function completeLogin(code) {
98
+ const pending = readPending();
99
+ if (!pending) {
100
+ throw new AxiError("No pending login to complete", "AUTH", [
101
+ "Run `otter-axi auth login` to start over",
102
+ ]);
103
+ }
104
+ const disc = await discover();
105
+ const { client } = await ensureClient(readConfig(), disc);
106
+ let tokens;
107
+ try {
108
+ tokens = await exchangeAuthorization(disc.authServer, {
109
+ // Pass metadata so the SDK uses the real token_endpoint (/oauth/token)
110
+ // rather than guessing /token, which nginx rejects with 405.
111
+ metadata: disc.asMetadata,
112
+ clientInformation: client,
113
+ authorizationCode: code,
114
+ codeVerifier: pending.verifier,
115
+ redirectUri: pending.redirect_uri,
116
+ resource: new URL(pending.resource),
117
+ });
118
+ }
119
+ catch (e) {
120
+ throw new AxiError(`Token exchange failed: ${e.message}`, "AUTH", [
121
+ "Authorization codes are short-lived — run `otter-axi auth login` and approve promptly",
122
+ ]);
123
+ }
124
+ writeConfig({ ...readConfig(), tokens: tokensToConfig(tokens) });
125
+ clearPending();
126
+ }
127
+ /** Refresh the stored access token using the refresh_token grant. Persists the result. */
128
+ export async function refreshTokens() {
129
+ const cfg = readConfig();
130
+ if (!cfg.tokens?.refresh_token || !cfg.client?.client_id) {
131
+ throw new AxiError("Cannot refresh — not fully logged in", "AUTH", [
132
+ "Run `otter-axi auth login`",
133
+ ]);
134
+ }
135
+ const disc = await discover();
136
+ let tokens;
137
+ try {
138
+ tokens = await refreshAuthorization(disc.authServer, {
139
+ metadata: disc.asMetadata,
140
+ clientInformation: {
141
+ client_id: cfg.client.client_id,
142
+ ...(cfg.client.client_secret
143
+ ? { client_secret: cfg.client.client_secret }
144
+ : {}),
145
+ },
146
+ refreshToken: cfg.tokens.refresh_token,
147
+ resource: new URL(disc.resource),
148
+ });
149
+ }
150
+ catch {
151
+ throw new AxiError("Session expired — please log in again", "AUTH", [
152
+ "Run `otter-axi auth login`",
153
+ ]);
154
+ }
155
+ // refreshAuthorization preserves the old refresh_token when the server omits a new one.
156
+ writeConfig({ ...readConfig(), tokens: tokensToConfig(tokens) });
157
+ }
158
+ /** Best-effort token revocation at the authorization server's revocation endpoint. */
159
+ export async function revokeToken() {
160
+ const cfg = readConfig();
161
+ const token = cfg.tokens?.refresh_token ?? cfg.tokens?.access_token;
162
+ if (!token || !cfg.client?.client_id)
163
+ return;
164
+ try {
165
+ const disc = await discover();
166
+ const endpoint = disc.asMetadata
167
+ .revocation_endpoint;
168
+ if (!endpoint)
169
+ return;
170
+ await fetch(endpoint, {
171
+ method: "POST",
172
+ headers: { "content-type": "application/x-www-form-urlencoded" },
173
+ body: new URLSearchParams({
174
+ token,
175
+ client_id: cfg.client.client_id,
176
+ }),
177
+ });
178
+ }
179
+ catch {
180
+ // Revocation is best-effort; clearing local tokens is what matters.
181
+ }
182
+ }
183
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../../src/otter/oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Output helpers. Handlers return plain objects (StructuredOutput) and the AXI SDK
3
+ * serializes them to TOON at the boundary — never print here.
4
+ */
5
+ export type StructuredOutput = Record<string, unknown>;
6
+ /** Default caps for token-frugal output. */
7
+ export declare const CELL_CHAR_CAP = 120;
8
+ export declare const PREVIEW_ROW_CAP = 20;
9
+ /** Truncate a cell value with a visible marker showing the full length. */
10
+ export declare function truncateCell(value: string, cap?: number): string;
11
+ /** Cap a list to `limit` rows, returning the kept rows and whether more exist. */
12
+ export declare function capList<T>(rows: T[], limit?: number): {
13
+ rows: T[];
14
+ total: number;
15
+ capped: boolean;
16
+ };
17
+ /** "N of M" when a total is known, else "N". */
18
+ export declare function countLabel(shown: number, total?: number): string;
19
+ /** Compact relative time like "2h ago" / "3d ago" from an ISO/epoch input. */
20
+ export declare function relativeTime(when: string | number | Date, now: Date): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Output helpers. Handlers return plain objects (StructuredOutput) and the AXI SDK
3
+ * serializes them to TOON at the boundary — never print here.
4
+ */
5
+ /** Default caps for token-frugal output. */
6
+ export const CELL_CHAR_CAP = 120;
7
+ export const PREVIEW_ROW_CAP = 20;
8
+ /** Truncate a cell value with a visible marker showing the full length. */
9
+ export function truncateCell(value, cap = CELL_CHAR_CAP) {
10
+ if (value.length <= cap)
11
+ return value;
12
+ return `${value.slice(0, cap)}…(truncated, ${value.length} chars total)`;
13
+ }
14
+ /** Cap a list to `limit` rows, returning the kept rows and whether more exist. */
15
+ export function capList(rows, limit = PREVIEW_ROW_CAP) {
16
+ if (rows.length <= limit)
17
+ return { rows, total: rows.length, capped: false };
18
+ return { rows: rows.slice(0, limit), total: rows.length, capped: true };
19
+ }
20
+ /** "N of M" when a total is known, else "N". */
21
+ export function countLabel(shown, total) {
22
+ if (total === undefined || total === shown)
23
+ return `${shown}`;
24
+ return `${shown} of ${total}`;
25
+ }
26
+ /** Compact relative time like "2h ago" / "3d ago" from an ISO/epoch input. */
27
+ export function relativeTime(when, now) {
28
+ const then = when instanceof Date ? when : new Date(when);
29
+ const ms = now.getTime() - then.getTime();
30
+ if (!Number.isFinite(ms))
31
+ return "unknown";
32
+ const s = Math.round(ms / 1000);
33
+ if (s < 60)
34
+ return `${s}s ago`;
35
+ const m = Math.round(s / 60);
36
+ if (m < 60)
37
+ return `${m}m ago`;
38
+ const h = Math.round(m / 60);
39
+ if (h < 24)
40
+ return `${h}h ago`;
41
+ const d = Math.round(h / 24);
42
+ return `${d}d ago`;
43
+ }
44
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/output.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,4CAA4C;AAC5C,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC;AACjC,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAElC,2EAA2E;AAC3E,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,GAAG,GAAG,aAAa;IAC7D,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,gBAAgB,KAAK,CAAC,MAAM,eAAe,CAAC;AAC3E,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,OAAO,CACrB,IAAS,EACT,KAAK,GAAG,eAAe;IAEvB,IAAI,IAAI,CAAC,MAAM,IAAI,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC7E,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1E,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,KAAc;IACtD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,GAAG,KAAK,EAAE,CAAC;IAC9D,OAAO,GAAG,KAAK,OAAO,KAAK,EAAE,CAAC;AAChC,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,YAAY,CAAC,IAA4B,EAAE,GAAS;IAClE,MAAM,IAAI,GAAG,IAAI,YAAY,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1D,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;IAC1C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,GAAG,CAAC,OAAO,CAAC;IAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IAC7B,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,GAAG,CAAC,OAAO,CAAC;IAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IAC7B,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,GAAG,CAAC,OAAO,CAAC;IAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IAC7B,OAAO,GAAG,CAAC,OAAO,CAAC;AACrB,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Transcript parser. otter-axi owns this so consumers don't re-derive a fragile `[ts] speaker:`
3
+ * regex (see specs/principles.md). The parse is best-effort on *structure* but lossless on
4
+ * *content*: every character of the source survives — unrecognized lines are preserved as
5
+ * continuations of the current segment, never dropped.
6
+ */
7
+ export interface Segment {
8
+ /** `H:MM:SS` timestamp without brackets, or "" for leading pre-timestamp text. */
9
+ start: string;
10
+ /** Speaker label as written ("Speaker 1" or a real name), or "" when unknown. */
11
+ speaker: string;
12
+ text: string;
13
+ }
14
+ /** Parse an Otter transcript string into ordered segments (lossless on content). */
15
+ export declare function parseTranscript(transcript: string): Segment[];
16
+ /** Reassemble segments into the original transcript text (inverse of parseTranscript). */
17
+ export declare function segmentsToText(segments: Segment[]): string;
18
+ /** Serialize segments to RFC-4180 CSV with a `start,speaker,text` header. */
19
+ export declare function segmentsToCsv(segments: Segment[]): string;
20
+ /** Serialize segments to TSV; tabs/newlines inside text are escaped so rows stay one-per-line. */
21
+ export declare function segmentsToTsv(segments: Segment[]): string;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Transcript parser. otter-axi owns this so consumers don't re-derive a fragile `[ts] speaker:`
3
+ * regex (see specs/principles.md). The parse is best-effort on *structure* but lossless on
4
+ * *content*: every character of the source survives — unrecognized lines are preserved as
5
+ * continuations of the current segment, never dropped.
6
+ */
7
+ const LINE = /^\[(\d+:\d{1,2}:\d{2})\]\s+(.+?):\s?(.*)$/;
8
+ /** Parse an Otter transcript string into ordered segments (lossless on content). */
9
+ export function parseTranscript(transcript) {
10
+ const segments = [];
11
+ let current;
12
+ const lines = transcript.split("\n");
13
+ lines.forEach((line, i) => {
14
+ const m = line.match(LINE);
15
+ if (m) {
16
+ current = { start: m[1], speaker: m[2], text: m[3] };
17
+ segments.push(current);
18
+ return;
19
+ }
20
+ // Continuation / blank / pre-timestamp line: append verbatim to the current segment.
21
+ if (!current) {
22
+ current = { start: "", speaker: "", text: line };
23
+ segments.push(current);
24
+ }
25
+ else {
26
+ current.text += `\n${line}`;
27
+ }
28
+ void i;
29
+ });
30
+ return segments;
31
+ }
32
+ /** Reassemble segments into the original transcript text (inverse of parseTranscript). */
33
+ export function segmentsToText(segments) {
34
+ return segments
35
+ .map((s) => (s.start ? `[${s.start}] ${s.speaker}: ${s.text}` : s.text))
36
+ .join("\n");
37
+ }
38
+ function csvField(value) {
39
+ // RFC 4180: quote when the field contains a comma, quote, CR or LF; double internal quotes.
40
+ if (/[",\r\n]/.test(value))
41
+ return `"${value.replace(/"/g, '""')}"`;
42
+ return value;
43
+ }
44
+ /** Serialize segments to RFC-4180 CSV with a `start,speaker,text` header. */
45
+ export function segmentsToCsv(segments) {
46
+ const rows = ["start,speaker,text"];
47
+ for (const s of segments) {
48
+ rows.push([s.start, s.speaker, s.text].map(csvField).join(","));
49
+ }
50
+ return `${rows.join("\r\n")}\r\n`;
51
+ }
52
+ /** Serialize segments to TSV; tabs/newlines inside text are escaped so rows stay one-per-line. */
53
+ export function segmentsToTsv(segments) {
54
+ const esc = (v) => v.replace(/\\/g, "\\\\").replace(/\t/g, "\\t").replace(/\r?\n/g, "\\n");
55
+ const rows = ["start\tspeaker\ttext"];
56
+ for (const s of segments) {
57
+ rows.push([s.start, s.speaker, s.text].map(esc).join("\t"));
58
+ }
59
+ return `${rows.join("\n")}\n`;
60
+ }
61
+ //# sourceMappingURL=transcript.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript.js","sourceRoot":"","sources":["../../src/transcript.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "otter-axi",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Find and pull Otter.ai meeting transcripts from the terminal with token-efficient output.",
6
+ "bin": {
7
+ "otter-axi": "dist/bin/otter-axi.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "skills/otter-axi",
12
+ "LICENSE",
13
+ "README.md"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "license": "MIT",
22
+ "scripts": {
23
+ "build": "tsc && chmod +x dist/bin/otter-axi.js",
24
+ "dev": "bun bin/otter-axi.ts",
25
+ "test": "vitest run --passWithNoTests",
26
+ "test:watch": "vitest",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.20.0",
31
+ "@toon-format/toon": "^2.3.0",
32
+ "axi-sdk-js": "^0.1.7"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^25.9.3",
36
+ "typescript": "^6.0.3",
37
+ "vitest": "^4.1.9"
38
+ },
39
+ "author": "Jarvus Innovations",
40
+ "keywords": [
41
+ "otter",
42
+ "otter.ai",
43
+ "axi",
44
+ "cli",
45
+ "agent",
46
+ "transcripts",
47
+ "mcp",
48
+ "toon"
49
+ ]
50
+ }
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: otter-axi
3
+ description: "AXI-compliant CLI for Otter.ai — find and pull meeting transcripts from the terminal with token-efficient TOON output. Use when the user wants to search their Otter meetings (by keyword, date range, title, attendee, channel, or folder), browse recent conversations, or pull a full meeting transcript by id/URL. Wraps Otter's hosted MCP server over OAuth; prefer this over the hosted MCP connector for scriptable, headless access."
4
+ user-invocable: false
5
+ author: Jarvus Innovations
6
+ metadata:
7
+ hermes:
8
+ tags: [otter, transcripts, meetings, notes, mcp]
9
+ category: productivity
10
+ ---
11
+
12
+ # otter-axi
13
+
14
+ Agent-ergonomic CLI that wraps Otter.ai's hosted MCP server to **find and pull meeting
15
+ transcripts**. Run `otter-axi --help` for the command list and `otter-axi <command> --help`
16
+ for any command's usage. It does find-and-pull only — analysis is the caller's job.
17
+
18
+ ## Setup
19
+
20
+ One-time browser approval, then headless forever after (tokens refresh silently):
21
+
22
+ ```sh
23
+ otter-axi auth login # opens your browser; approve access
24
+ otter-axi doctor # verify: config → credentials → MCP reachable
25
+ ```
26
+
27
+ Agents driving the login over a relay: `otter-axi auth login --no-wait` prints the authorize
28
+ URL and returns; relay it, then run `otter-axi auth login --wait` in a separate turn to capture
29
+ the redirect. Authorization codes are short-lived — approve promptly.
30
+
31
+ Tokens live in `~/.config/otter-axi/config.json` (mode 0600) and are never printed.
32
+
33
+ ## Core workflow
34
+
35
+ 1. **Find / browse** with `search` — returns meeting *metadata* (title, date, duration,
36
+ one-line summary, action-item count, and the id). An empty query plus a date range is
37
+ browse mode.
38
+
39
+ ```sh
40
+ otter-axi search "pricing discussion" --after 30d
41
+ otter-axi search --after 2026/05/01 --before 2026/05/07 # browse a window
42
+ otter-axi search --in-transcript "roadmap,milestones" --attended-by "Jane Doe"
43
+ ```
44
+
45
+ Flags: `-q/--query`, `--after`/`--before` (ISO `2026-05-01` or relative `7d`/`2w`/`3m`),
46
+ `--title-contains`, `--in-transcript`, `--attended-by`, `--channel`, `--folder`, `--mine`,
47
+ `--limit`, `--full`.
48
+
49
+ 2. **Pull a transcript** with `fetch` — takes an id (from search) or an `otter.ai/u/<id>` URL:
50
+
51
+ ```sh
52
+ otter-axi fetch <id> # metadata header + preview
53
+ otter-axi fetch <id> --full # verbatim transcript to stdout (pipe it)
54
+ otter-axi fetch <id> --text-out t.txt # verbatim transcript to a file
55
+ otter-axi fetch <id> --json-out t.json # parsed segments [{start,speaker,text}]
56
+ otter-axi fetch <id> --csv-out t.csv # parsed segments as CSV (--tsv-out for TSV)
57
+ ```
58
+
59
+ Transcripts are `[H:MM:SS] Speaker N: …`. Default/`--full`/`--text-out` are verbatim; the
60
+ `--json-out`/`--csv-out`/`--tsv-out` modes parse into `{start,speaker,text}` segments via
61
+ otter-axi's own lossless parser (so you don't re-derive it). At most one output mode per call.
62
+
63
+ ## Notes
64
+
65
+ - Read-only: scopes are `profile:read` + `conversations:read`. No write/delete operations exist.
66
+ - `otter-axi setup hooks` installs the SessionStart hook (Claude Code / Codex / OpenCode) that
67
+ surfaces the home view at session start; `OTTER_AXI_DISABLE_HOOKS=1` opts out.
68
+ - Install ad hoc with `npx -y otter-axi <command>`.