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.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/bin/otter-axi.d.ts +2 -0
- package/dist/bin/otter-axi.js +4 -0
- package/dist/bin/otter-axi.js.map +1 -0
- package/dist/src/cli.d.ts +5 -0
- package/dist/src/cli.js +50 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/commands/auth.d.ts +3 -0
- package/dist/src/commands/auth.js +144 -0
- package/dist/src/commands/auth.js.map +1 -0
- package/dist/src/commands/doctor.d.ts +3 -0
- package/dist/src/commands/doctor.js +47 -0
- package/dist/src/commands/doctor.js.map +1 -0
- package/dist/src/commands/fetch.d.ts +5 -0
- package/dist/src/commands/fetch.js +132 -0
- package/dist/src/commands/fetch.js.map +1 -0
- package/dist/src/commands/home.d.ts +7 -0
- package/dist/src/commands/home.js +28 -0
- package/dist/src/commands/home.js.map +1 -0
- package/dist/src/commands/search.d.ts +3 -0
- package/dist/src/commands/search.js +105 -0
- package/dist/src/commands/search.js.map +1 -0
- package/dist/src/commands/setup.d.ts +6 -0
- package/dist/src/commands/setup.js +25 -0
- package/dist/src/commands/setup.js.map +1 -0
- package/dist/src/config.d.ts +62 -0
- package/dist/src/config.js +89 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/dates.d.ts +5 -0
- package/dist/src/dates.js +36 -0
- package/dist/src/dates.js.map +1 -0
- package/dist/src/flags.d.ts +19 -0
- package/dist/src/flags.js +45 -0
- package/dist/src/flags.js.map +1 -0
- package/dist/src/meta.d.ts +3 -0
- package/dist/src/meta.js +22 -0
- package/dist/src/meta.js.map +1 -0
- package/dist/src/otter/client.d.ts +30 -0
- package/dist/src/otter/client.js +120 -0
- package/dist/src/otter/client.js.map +1 -0
- package/dist/src/otter/loopback.d.ts +13 -0
- package/dist/src/otter/loopback.js +68 -0
- package/dist/src/otter/loopback.js.map +1 -0
- package/dist/src/otter/oauth.d.ts +32 -0
- package/dist/src/otter/oauth.js +183 -0
- package/dist/src/otter/oauth.js.map +1 -0
- package/dist/src/output.d.ts +20 -0
- package/dist/src/output.js +44 -0
- package/dist/src/output.js.map +1 -0
- package/dist/src/transcript.d.ts +21 -0
- package/dist/src/transcript.js +61 -0
- package/dist/src/transcript.js.map +1 -0
- package/package.json +50 -0
- package/skills/otter-axi/SKILL.md +68 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readConfig } from "../config.js";
|
|
2
|
+
import { normalizeDate } from "../dates.js";
|
|
3
|
+
import { hasFlag, numFlag, parseArgs, strFlag } from "../flags.js";
|
|
4
|
+
import { search as searchMeetings } from "../otter/client.js";
|
|
5
|
+
import { countLabel, truncateCell } from "../output.js";
|
|
6
|
+
export const SEARCH_HELP = `usage: otter-axi search [query...] [flags]
|
|
7
|
+
flags:
|
|
8
|
+
-q, --query <str> keyword/semantic query (or pass positionally; may be empty)
|
|
9
|
+
--after <date> created on/after — ISO (2026-05-01) or relative (7d, 30d)
|
|
10
|
+
--before <date> created on/before — same formats
|
|
11
|
+
--title-contains <str> space-separated keywords matched against the title
|
|
12
|
+
--in-transcript <str> comma-separated keywords searched within transcripts
|
|
13
|
+
--attended-by <name> comma-separated attendee names (not emails)
|
|
14
|
+
--channel <name> comma-separated channel name(s) to search within
|
|
15
|
+
--folder <name> comma-separated folder name(s) to search within
|
|
16
|
+
--mine restrict to your own meetings (exclude shared)
|
|
17
|
+
--limit <n> cap rows shown (default 20)
|
|
18
|
+
--full show all rows, untruncated
|
|
19
|
+
notes:
|
|
20
|
+
empty query + a date range is browse mode — list meetings in a window`;
|
|
21
|
+
const VALUED = [
|
|
22
|
+
"query",
|
|
23
|
+
"q",
|
|
24
|
+
"after",
|
|
25
|
+
"before",
|
|
26
|
+
"title-contains",
|
|
27
|
+
"in-transcript",
|
|
28
|
+
"attended-by",
|
|
29
|
+
"channel",
|
|
30
|
+
"folder",
|
|
31
|
+
"limit",
|
|
32
|
+
];
|
|
33
|
+
export async function searchCommand(args) {
|
|
34
|
+
const parsed = parseArgs(args, { valued: VALUED });
|
|
35
|
+
const query = parsed.positionals.join(" ") || strFlag(parsed, "query") || strFlag(parsed, "q") || "";
|
|
36
|
+
const params = { query };
|
|
37
|
+
const after = strFlag(parsed, "after");
|
|
38
|
+
if (after)
|
|
39
|
+
params.created_after = normalizeDate(after);
|
|
40
|
+
const before = strFlag(parsed, "before");
|
|
41
|
+
if (before)
|
|
42
|
+
params.created_before = normalizeDate(before);
|
|
43
|
+
const titleContains = strFlag(parsed, "title-contains");
|
|
44
|
+
if (titleContains)
|
|
45
|
+
params.title_contains = titleContains;
|
|
46
|
+
const inTranscript = strFlag(parsed, "in-transcript");
|
|
47
|
+
if (inTranscript)
|
|
48
|
+
params.keywords_in_transcript = inTranscript;
|
|
49
|
+
const attendedBy = strFlag(parsed, "attended-by");
|
|
50
|
+
if (attendedBy)
|
|
51
|
+
params.attended_by = attendedBy;
|
|
52
|
+
const channel = strFlag(parsed, "channel");
|
|
53
|
+
if (channel)
|
|
54
|
+
params.channel_name = channel;
|
|
55
|
+
const folder = strFlag(parsed, "folder");
|
|
56
|
+
if (folder)
|
|
57
|
+
params.folder_name = folder;
|
|
58
|
+
if (hasFlag(parsed, "mine"))
|
|
59
|
+
params.include_shared_meetings = false;
|
|
60
|
+
// username (from cache) lets the upstream compute participation_status — not a flag.
|
|
61
|
+
const cachedName = readConfig().user?.name;
|
|
62
|
+
if (cachedName)
|
|
63
|
+
params.username = cachedName;
|
|
64
|
+
const limit = numFlag(parsed, "limit") ?? 20;
|
|
65
|
+
const full = hasFlag(parsed, "full");
|
|
66
|
+
const meetings = await searchMeetings(params);
|
|
67
|
+
if (meetings.length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
matched: 0,
|
|
70
|
+
result: "0 meetings found",
|
|
71
|
+
filters: describeFilters(query, params),
|
|
72
|
+
help: [
|
|
73
|
+
"Widen the date window (e.g. --after 90d) or relax filters",
|
|
74
|
+
"Empty query + a date range browses everything in that window",
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const shown = full ? meetings : meetings.slice(0, limit);
|
|
79
|
+
const rows = shown.map((m) => ({
|
|
80
|
+
id: m.id,
|
|
81
|
+
title: full ? (m.title ?? "") : truncateCell(m.title ?? "", 60),
|
|
82
|
+
start: (m.start_time ?? "").slice(0, 10),
|
|
83
|
+
dur: m.duration ?? "—",
|
|
84
|
+
summary: full ? (m.short_summary ?? "") : truncateCell(m.short_summary ?? "", 80),
|
|
85
|
+
ai: Array.isArray(m.action_items) ? m.action_items.length : 0,
|
|
86
|
+
}));
|
|
87
|
+
const out = {
|
|
88
|
+
matched: meetings.length,
|
|
89
|
+
...(shown.length < meetings.length ? { shown: shown.length } : {}),
|
|
90
|
+
count: countLabel(shown.length, meetings.length),
|
|
91
|
+
meetings: rows,
|
|
92
|
+
help: ["Run `otter-axi fetch <id>` to pull a transcript"],
|
|
93
|
+
};
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
function describeFilters(query, params) {
|
|
97
|
+
const parts = [`query="${query}"`];
|
|
98
|
+
for (const [k, v] of Object.entries(params)) {
|
|
99
|
+
if (k === "query" || k === "username")
|
|
100
|
+
continue;
|
|
101
|
+
parts.push(`${k}=${String(v)}`);
|
|
102
|
+
}
|
|
103
|
+
return parts.join(", ");
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../../src/commands/search.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StructuredOutput } from "../output.js";
|
|
2
|
+
export declare const SETUP_HELP = "usage: otter-axi setup hooks\nInstalls the SessionStart hooks (Claude Code, Codex, OpenCode) that inject the\notter-axi home view into agent sessions. Idempotent; needs no credentials.";
|
|
3
|
+
export declare function setupCommand(args: string[], opts?: {
|
|
4
|
+
homeDir?: string;
|
|
5
|
+
shouldInstall?: (execPath: string) => boolean;
|
|
6
|
+
}): Promise<StructuredOutput>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AxiError, installSessionStartHooks } from "axi-sdk-js";
|
|
2
|
+
export const SETUP_HELP = `usage: otter-axi setup hooks
|
|
3
|
+
Installs the SessionStart hooks (Claude Code, Codex, OpenCode) that inject the
|
|
4
|
+
otter-axi home view into agent sessions. Idempotent; needs no credentials.`;
|
|
5
|
+
export async function setupCommand(args, opts = {}) {
|
|
6
|
+
if (args[0] !== "hooks") {
|
|
7
|
+
throw new AxiError("Unknown setup subcommand", "VALIDATION_ERROR", [
|
|
8
|
+
"Run `otter-axi setup hooks`",
|
|
9
|
+
]);
|
|
10
|
+
}
|
|
11
|
+
installSessionStartHooks({
|
|
12
|
+
marker: "otter-axi",
|
|
13
|
+
binaryNames: ["otter-axi"],
|
|
14
|
+
...(opts.homeDir ? { homeDir: opts.homeDir } : {}),
|
|
15
|
+
...(opts.shouldInstall ? { shouldInstall: opts.shouldInstall } : {}),
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
setup: "hooks installed (or already up to date)",
|
|
19
|
+
help: [
|
|
20
|
+
"Start a new agent session to see the otter-axi home view injected",
|
|
21
|
+
"Run `otter-axi auth login` if you have not connected your Otter account",
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/commands/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAGhE,MAAM,CAAC,MAAM,UAAU,GAAG;;2EAEiD,CAAC;AAE5E,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAc,EACd,OAA4E,EAAE;IAE9E,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,QAAQ,CAAC,0BAA0B,EAAE,kBAAkB,EAAE;YACjE,6BAA6B;SAC9B,CAAC,CAAC;IACL,CAAC;IACD,wBAAwB,CAAC;QACvB,MAAM,EAAE,WAAW;QACnB,WAAW,EAAE,CAAC,WAAW,CAAC;QAC1B,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClD,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACrE,CAAC,CAAC;IACH,OAAO;QACL,KAAK,EAAE,yCAAyC;QAChD,IAAI,EAAE;YACJ,mEAAmE;YACnE,yEAAyE;SAC1E;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export declare const CONFIG_VERSION = 1;
|
|
2
|
+
/**
|
|
3
|
+
* otter-axi authenticates to a single Otter account via OAuth, so the config holds one
|
|
4
|
+
* credential set (not the multi-profile model of instance-based tools). Secrets live in
|
|
5
|
+
* `tokens` / `client`; `user` is a non-secret cache for fast home/doctor rendering.
|
|
6
|
+
*/
|
|
7
|
+
/** A dynamically-registered OAuth public client (RFC 7591). */
|
|
8
|
+
export interface OAuthClient {
|
|
9
|
+
client_id: string;
|
|
10
|
+
client_secret?: string;
|
|
11
|
+
redirect_uri?: string;
|
|
12
|
+
registered_at?: string;
|
|
13
|
+
}
|
|
14
|
+
/** The OAuth token set. `expires_at` is epoch milliseconds. */
|
|
15
|
+
export interface OAuthTokens {
|
|
16
|
+
access_token: string;
|
|
17
|
+
refresh_token?: string;
|
|
18
|
+
token_type?: string;
|
|
19
|
+
scope?: string;
|
|
20
|
+
expires_at?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Cached, non-secret profile of the authenticated user. */
|
|
23
|
+
export interface UserCache {
|
|
24
|
+
name?: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
cached_at?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface Config {
|
|
29
|
+
version: number;
|
|
30
|
+
client?: OAuthClient;
|
|
31
|
+
tokens?: OAuthTokens;
|
|
32
|
+
user?: UserCache;
|
|
33
|
+
}
|
|
34
|
+
export declare function configDir(): string;
|
|
35
|
+
export declare function configPath(): string;
|
|
36
|
+
export declare function defaultConfig(): Config;
|
|
37
|
+
export declare function readConfig(): Config;
|
|
38
|
+
export declare function writeConfig(cfg: Config): void;
|
|
39
|
+
/** Remove the stored token set (used by `auth logout`). Idempotent. */
|
|
40
|
+
export declare function clearTokens(cfg: Config): Config;
|
|
41
|
+
/** Delete the entire config file. Idempotent. */
|
|
42
|
+
export declare function deleteConfig(): void;
|
|
43
|
+
/** True when a token set is present (logged in, possibly expired). */
|
|
44
|
+
export declare function isLoggedIn(cfg: Config): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* In-flight login state persisted between `auth login --no-wait` (prepare) and
|
|
47
|
+
* `auth login --wait` (bind loopback + exchange). Holds the PKCE verifier and the exact
|
|
48
|
+
* redirect_uri/port so the second phase — possibly a separate process — can complete.
|
|
49
|
+
*/
|
|
50
|
+
export interface PendingAuth {
|
|
51
|
+
verifier: string;
|
|
52
|
+
redirect_uri: string;
|
|
53
|
+
port: number;
|
|
54
|
+
resource: string;
|
|
55
|
+
auth_server: string;
|
|
56
|
+
url: string;
|
|
57
|
+
expires_at: number;
|
|
58
|
+
}
|
|
59
|
+
export declare function pendingPath(): string;
|
|
60
|
+
export declare function readPending(): PendingAuth | undefined;
|
|
61
|
+
export declare function writePending(pending: PendingAuth): void;
|
|
62
|
+
export declare function clearPending(): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export const CONFIG_VERSION = 1;
|
|
5
|
+
// Paths ──────────────────────────────────────────────────────────────
|
|
6
|
+
export function configDir() {
|
|
7
|
+
if (process.env.OTTER_AXI_CONFIG_DIR) {
|
|
8
|
+
return process.env.OTTER_AXI_CONFIG_DIR;
|
|
9
|
+
}
|
|
10
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
11
|
+
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
|
|
12
|
+
return join(base, "otter-axi");
|
|
13
|
+
}
|
|
14
|
+
export function configPath() {
|
|
15
|
+
return join(configDir(), "config.json");
|
|
16
|
+
}
|
|
17
|
+
// Read / write ────────────────────────────────────────────────────────
|
|
18
|
+
export function defaultConfig() {
|
|
19
|
+
return { version: CONFIG_VERSION };
|
|
20
|
+
}
|
|
21
|
+
export function readConfig() {
|
|
22
|
+
const path = configPath();
|
|
23
|
+
if (!existsSync(path))
|
|
24
|
+
return defaultConfig();
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
27
|
+
return {
|
|
28
|
+
version: parsed.version ?? CONFIG_VERSION,
|
|
29
|
+
client: parsed.client,
|
|
30
|
+
tokens: parsed.tokens,
|
|
31
|
+
user: parsed.user,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return defaultConfig();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function writeConfig(cfg) {
|
|
39
|
+
const dir = configDir();
|
|
40
|
+
if (!existsSync(dir))
|
|
41
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
42
|
+
// Restrictive perms: the file holds OAuth access/refresh tokens.
|
|
43
|
+
writeFileSync(configPath(), `${JSON.stringify(cfg, null, 2)}\n`, {
|
|
44
|
+
mode: 0o600,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Remove the stored token set (used by `auth logout`). Idempotent. */
|
|
48
|
+
export function clearTokens(cfg) {
|
|
49
|
+
const { tokens: _tokens, ...rest } = cfg;
|
|
50
|
+
return { ...rest };
|
|
51
|
+
}
|
|
52
|
+
/** Delete the entire config file. Idempotent. */
|
|
53
|
+
export function deleteConfig() {
|
|
54
|
+
const path = configPath();
|
|
55
|
+
if (existsSync(path))
|
|
56
|
+
rmSync(path);
|
|
57
|
+
}
|
|
58
|
+
/** True when a token set is present (logged in, possibly expired). */
|
|
59
|
+
export function isLoggedIn(cfg) {
|
|
60
|
+
return Boolean(cfg.tokens?.access_token);
|
|
61
|
+
}
|
|
62
|
+
export function pendingPath() {
|
|
63
|
+
return join(configDir(), "pending-auth.json");
|
|
64
|
+
}
|
|
65
|
+
export function readPending() {
|
|
66
|
+
const path = pendingPath();
|
|
67
|
+
if (!existsSync(path))
|
|
68
|
+
return undefined;
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function writePending(pending) {
|
|
77
|
+
const dir = configDir();
|
|
78
|
+
if (!existsSync(dir))
|
|
79
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
80
|
+
writeFileSync(pendingPath(), `${JSON.stringify(pending, null, 2)}\n`, {
|
|
81
|
+
mode: 0o600,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export function clearPending() {
|
|
85
|
+
const path = pendingPath();
|
|
86
|
+
if (existsSync(path))
|
|
87
|
+
rmSync(path);
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
function pad(n) {
|
|
3
|
+
return String(n).padStart(2, "0");
|
|
4
|
+
}
|
|
5
|
+
function toYmd(d) {
|
|
6
|
+
return `${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(d.getDate())}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a user-supplied date to the upstream `YYYY/MM/DD` format. Accepts ISO
|
|
10
|
+
* (`2026-05-01` or `2026/5/1`) and relative forms anchored to `now`: `7d`, `2w`, `3m`, `1y`.
|
|
11
|
+
*/
|
|
12
|
+
export function normalizeDate(input, now = new Date()) {
|
|
13
|
+
const s = input.trim();
|
|
14
|
+
const rel = s.match(/^(\d+)\s*([dwmy])$/i);
|
|
15
|
+
if (rel) {
|
|
16
|
+
const n = Number(rel[1]);
|
|
17
|
+
const unit = rel[2].toLowerCase();
|
|
18
|
+
const d = new Date(now);
|
|
19
|
+
if (unit === "d")
|
|
20
|
+
d.setDate(d.getDate() - n);
|
|
21
|
+
else if (unit === "w")
|
|
22
|
+
d.setDate(d.getDate() - n * 7);
|
|
23
|
+
else if (unit === "m")
|
|
24
|
+
d.setMonth(d.getMonth() - n);
|
|
25
|
+
else
|
|
26
|
+
d.setFullYear(d.getFullYear() - n);
|
|
27
|
+
return toYmd(d);
|
|
28
|
+
}
|
|
29
|
+
const iso = s.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$/);
|
|
30
|
+
if (iso)
|
|
31
|
+
return `${iso[1]}/${pad(Number(iso[2]))}/${pad(Number(iso[3]))}`;
|
|
32
|
+
throw new AxiError(`Unrecognized date: ${input}`, "VALIDATION_ERROR", [
|
|
33
|
+
"Use ISO (2026-05-01) or relative (7d, 2w, 3m, 1y)",
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=dates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dates.js","sourceRoot":"","sources":["../../src/dates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,SAAS,GAAG,CAAC,CAAS;IACpB,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,KAAK,CAAC,CAAO;IACpB,OAAO,GAAG,CAAC,CAAC,WAAW,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;AAC3E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,MAAY,IAAI,IAAI,EAAE;IACjE,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAEvB,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;IAC3C,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,IAAI,KAAK,GAAG;YAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;aACxC,IAAI,IAAI,KAAK,GAAG;YAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;aACjD,IAAI,IAAI,KAAK,GAAG;YAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;;YAC/C,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;QACxC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;IAC3D,IAAI,GAAG;QAAE,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE1E,MAAM,IAAI,QAAQ,CAAC,sBAAsB,KAAK,EAAE,EAAE,kBAAkB,EAAE;QACpE,mDAAmD;KACpD,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal argument parser for command handlers. Splits a command's args into positionals
|
|
3
|
+
* and flags. `--key=value` always yields a string value; `--key value` yields a string
|
|
4
|
+
* value only when `key` is in `valued` and the next token isn't a flag; otherwise `--key`
|
|
5
|
+
* is a boolean `true`. A lone `-` is a positional (the stdin marker).
|
|
6
|
+
*/
|
|
7
|
+
export interface ParsedArgs {
|
|
8
|
+
positionals: string[];
|
|
9
|
+
flags: Record<string, string | boolean>;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseArgs(args: string[], opts?: {
|
|
12
|
+
valued?: string[];
|
|
13
|
+
}): ParsedArgs;
|
|
14
|
+
/** Read a flag as a string, or undefined when absent / boolean-only. */
|
|
15
|
+
export declare function strFlag(parsed: ParsedArgs, name: string): string | undefined;
|
|
16
|
+
/** Read a flag as a number, or undefined. Throws-free: returns undefined on NaN. */
|
|
17
|
+
export declare function numFlag(parsed: ParsedArgs, name: string): number | undefined;
|
|
18
|
+
/** True when the flag is present in any form (boolean true or a string value). */
|
|
19
|
+
export declare function hasFlag(parsed: ParsedArgs, name: string): boolean;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function parseArgs(args, opts = {}) {
|
|
2
|
+
const valued = new Set(opts.valued ?? []);
|
|
3
|
+
const positionals = [];
|
|
4
|
+
const flags = {};
|
|
5
|
+
for (let i = 0; i < args.length; i++) {
|
|
6
|
+
const token = args[i];
|
|
7
|
+
if (token === "-" || !token.startsWith("--")) {
|
|
8
|
+
positionals.push(token);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const body = token.slice(2);
|
|
12
|
+
const eq = body.indexOf("=");
|
|
13
|
+
if (eq >= 0) {
|
|
14
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const next = args[i + 1];
|
|
18
|
+
if (valued.has(body) && next !== undefined && !next.startsWith("--")) {
|
|
19
|
+
flags[body] = next;
|
|
20
|
+
i++;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
flags[body] = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { positionals, flags };
|
|
27
|
+
}
|
|
28
|
+
/** Read a flag as a string, or undefined when absent / boolean-only. */
|
|
29
|
+
export function strFlag(parsed, name) {
|
|
30
|
+
const v = parsed.flags[name];
|
|
31
|
+
return typeof v === "string" ? v : undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Read a flag as a number, or undefined. Throws-free: returns undefined on NaN. */
|
|
34
|
+
export function numFlag(parsed, name) {
|
|
35
|
+
const v = strFlag(parsed, name);
|
|
36
|
+
if (v === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
const n = Number(v);
|
|
39
|
+
return Number.isFinite(n) ? n : undefined;
|
|
40
|
+
}
|
|
41
|
+
/** True when the flag is present in any form (boolean true or a string value). */
|
|
42
|
+
export function hasFlag(parsed, name) {
|
|
43
|
+
return parsed.flags[name] !== undefined;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=flags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flags.js","sourceRoot":"","sources":["../../src/flags.ts"],"names":[],"mappings":"AAWA,MAAM,UAAU,SAAS,CACvB,IAAc,EACd,OAA8B,EAAE;IAEhC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,MAAM,KAAK,GAAqC,EAAE,CAAC;IAEnD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,KAAK,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7C,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACxB,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YACZ,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;YAC9C,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACzB,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACrE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;YACnB,CAAC,EAAE,CAAC;QACN,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;QACrB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;AAChC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,OAAO,CAAC,MAAkB,EAAE,IAAY;IACtD,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7B,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/C,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,OAAO,CAAC,MAAkB,EAAE,IAAY;IACtD,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACtC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5C,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,OAAO,CAAC,MAAkB,EAAE,IAAY;IACtD,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;AAC1C,CAAC"}
|
package/dist/src/meta.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export const DESCRIPTION = "Find and pull Otter.ai meeting transcripts from the terminal with token-efficient output";
|
|
5
|
+
/** Read the package version from the nearest package.json (dist or src layout). */
|
|
6
|
+
export function readVersion() {
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
for (const candidate of [
|
|
9
|
+
join(here, "..", "package.json"),
|
|
10
|
+
join(here, "..", "..", "package.json"),
|
|
11
|
+
join(here, "..", "..", "..", "package.json"),
|
|
12
|
+
]) {
|
|
13
|
+
if (!existsSync(candidate))
|
|
14
|
+
continue;
|
|
15
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
16
|
+
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
17
|
+
return parsed.version;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return "0.0.0";
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","sourceRoot":"","sources":["../../src/meta.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,WAAW,GACtB,0FAA0F,CAAC;AAE7F,mFAAmF;AACnF,MAAM,UAAU,WAAW;IACzB,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,KAAK,MAAM,SAAS,IAAI;QACtB,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC;QAChC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC;QACtC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC;KAC7C,EAAE,CAAC;QACF,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,SAAS;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAEzD,CAAC;QACF,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpE,OAAO,MAAM,CAAC,OAAO,CAAC;QACxB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface UserInfo {
|
|
2
|
+
name?: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
datetime?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Meeting {
|
|
7
|
+
id: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
start_time?: string;
|
|
11
|
+
/** Pre-humanized by upstream, e.g. "3h 51m". */
|
|
12
|
+
duration?: string;
|
|
13
|
+
short_summary?: string;
|
|
14
|
+
action_items?: unknown[];
|
|
15
|
+
}
|
|
16
|
+
export interface Transcript {
|
|
17
|
+
id: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
text: string;
|
|
21
|
+
metadata?: {
|
|
22
|
+
action_items?: unknown[];
|
|
23
|
+
duration?: string;
|
|
24
|
+
short_summary?: string;
|
|
25
|
+
start_time?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function getUser(): Promise<UserInfo>;
|
|
29
|
+
export declare function search(params: Record<string, unknown>): Promise<Meeting[]>;
|
|
30
|
+
export declare function fetchTranscript(id: string): Promise<Transcript>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { readConfig } from "../config.js";
|
|
5
|
+
import { readVersion } from "../meta.js";
|
|
6
|
+
import { MCP_URL, refreshTokens } from "./oauth.js";
|
|
7
|
+
/** Return a valid access token, refreshing first if it's within 60s of expiry. */
|
|
8
|
+
async function validAccessToken() {
|
|
9
|
+
const cfg = readConfig();
|
|
10
|
+
if (!cfg.tokens?.access_token) {
|
|
11
|
+
throw new AxiError("Not logged in", "AUTH", [
|
|
12
|
+
"Run `otter-axi auth login` to connect your Otter account",
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
const { expires_at, refresh_token } = cfg.tokens;
|
|
16
|
+
if (expires_at && refresh_token && expires_at - Date.now() < 60_000) {
|
|
17
|
+
await refreshTokens();
|
|
18
|
+
return readConfig().tokens?.access_token ?? cfg.tokens.access_token;
|
|
19
|
+
}
|
|
20
|
+
return cfg.tokens.access_token;
|
|
21
|
+
}
|
|
22
|
+
function isUnauthorized(e) {
|
|
23
|
+
const msg = String(e?.message ?? e);
|
|
24
|
+
return /401|unauthor/i.test(msg);
|
|
25
|
+
}
|
|
26
|
+
async function connect(token) {
|
|
27
|
+
const transport = new StreamableHTTPClientTransport(new URL(MCP_URL), {
|
|
28
|
+
requestInit: { headers: { Authorization: `Bearer ${token}` } },
|
|
29
|
+
});
|
|
30
|
+
const client = new Client({ name: "otter-axi", version: readVersion() }, { capabilities: {} });
|
|
31
|
+
await client.connect(transport);
|
|
32
|
+
return { client, close: () => transport.close() };
|
|
33
|
+
}
|
|
34
|
+
/** Call an MCP tool, transparently refreshing + retrying once on a 401. */
|
|
35
|
+
async function callTool(name, args) {
|
|
36
|
+
const run = async (token) => {
|
|
37
|
+
const { client, close } = await connect(token);
|
|
38
|
+
try {
|
|
39
|
+
return await client.callTool({ name, arguments: args });
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await close().catch(() => { });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
let token = await validAccessToken();
|
|
46
|
+
try {
|
|
47
|
+
return await run(token);
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
if (isUnauthorized(e) && readConfig().tokens?.refresh_token) {
|
|
51
|
+
await refreshTokens();
|
|
52
|
+
token = readConfig().tokens?.access_token ?? token;
|
|
53
|
+
try {
|
|
54
|
+
return await run(token);
|
|
55
|
+
}
|
|
56
|
+
catch (e2) {
|
|
57
|
+
throw mapError(e2);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw mapError(e);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function mapError(e) {
|
|
64
|
+
if (e instanceof AxiError)
|
|
65
|
+
return e;
|
|
66
|
+
const msg = String(e?.message ?? e);
|
|
67
|
+
if (isUnauthorized(e)) {
|
|
68
|
+
return new AxiError("Authentication failed or session expired", "AUTH", [
|
|
69
|
+
"Run `otter-axi auth login`",
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
if (/429|rate limit/i.test(msg)) {
|
|
73
|
+
return new AxiError("Otter rate limit hit — retry shortly", "RATE_LIMIT", []);
|
|
74
|
+
}
|
|
75
|
+
return new AxiError(`Otter MCP request failed: ${msg}`, "UPSTREAM", []);
|
|
76
|
+
}
|
|
77
|
+
/** Pull the text from a single MCP `content[0].text` envelope. */
|
|
78
|
+
function textOf(envelope) {
|
|
79
|
+
const content = envelope?.content;
|
|
80
|
+
const first = content?.find((c) => c?.type === "text") ?? content?.[0];
|
|
81
|
+
return typeof first?.text === "string" ? first.text : undefined;
|
|
82
|
+
}
|
|
83
|
+
/** Pull the inner payload string from the MCP `content[0].text` envelope. */
|
|
84
|
+
function innerText(result) {
|
|
85
|
+
const text = textOf(result);
|
|
86
|
+
if (text === undefined) {
|
|
87
|
+
throw new AxiError("Unexpected response shape from Otter MCP", "UPSTREAM", []);
|
|
88
|
+
}
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The `search`/`fetch` tools double-wrap: `content[0].text` is itself an MCP content
|
|
93
|
+
* envelope whose inner `text` holds the real JSON payload. Unwrap that second layer when
|
|
94
|
+
* present; otherwise return the first-layer text unchanged (e.g. `get_user_info` prose).
|
|
95
|
+
*/
|
|
96
|
+
function payloadText(result) {
|
|
97
|
+
const text = innerText(result);
|
|
98
|
+
try {
|
|
99
|
+
const inner = textOf(JSON.parse(text));
|
|
100
|
+
if (inner !== undefined)
|
|
101
|
+
return inner;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// first-layer text isn't a JSON envelope — use it as-is
|
|
105
|
+
}
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
108
|
+
export async function getUser() {
|
|
109
|
+
const text = innerText(await callTool("get_user_info", {}));
|
|
110
|
+
const grab = (label) => text.match(new RegExp(`${label}:\\s*(.+)`))?.[1]?.trim();
|
|
111
|
+
return { name: grab("Name"), email: grab("Email"), datetime: grab("Current DateTime") };
|
|
112
|
+
}
|
|
113
|
+
export async function search(params) {
|
|
114
|
+
const parsed = JSON.parse(payloadText(await callTool("search", params)));
|
|
115
|
+
return parsed.results ?? [];
|
|
116
|
+
}
|
|
117
|
+
export async function fetchTranscript(id) {
|
|
118
|
+
return JSON.parse(payloadText(await callTool("fetch", { id })));
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/otter/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface Loopback {
|
|
2
|
+
port: number;
|
|
3
|
+
/** Resolves with the authorization code, or rejects on error / timeout. */
|
|
4
|
+
waitForCode(timeoutMs: number): Promise<string>;
|
|
5
|
+
close(): void;
|
|
6
|
+
}
|
|
7
|
+
/** Return the first port from `candidates` that is bindable on 127.0.0.1. */
|
|
8
|
+
export declare function pickPort(candidates: number[]): Promise<number>;
|
|
9
|
+
/**
|
|
10
|
+
* Start a loopback HTTP server on `127.0.0.1:port` that captures the OAuth redirect at
|
|
11
|
+
* `/callback`. The browser response is sent UTF-8 (so the em-dash renders correctly).
|
|
12
|
+
*/
|
|
13
|
+
export declare function startLoopback(port: number): Promise<Loopback>;
|