samoagent 0.3.0 → 0.4.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/README.md CHANGED
@@ -14,13 +14,15 @@ Requirements:
14
14
  - `RECALL_API_KEY`.
15
15
  - `ngrok` installed and authenticated (free plan). `join` starts and manages ngrok automatically — you don't run it yourself.
16
16
 
17
+ Install the CLI from npm:
18
+
17
19
  ```bash
18
- bun install
20
+ npm install -g samoagent
19
21
  export RECALL_API_KEY=...
20
- bun run build
22
+ samoagent join "https://meet.google.com/..." --name Leo
21
23
  ```
22
24
 
23
- During development use `bun run samoagent ...`. After build or package install, use `samoagent ...`.
25
+ During development use `bun install`, `bun run build`, then `bun run samoagent ...`.
24
26
 
25
27
  ## What It Provides
26
28
 
@@ -35,6 +37,7 @@ samoagent gives an AI agent a small set of meeting tools:
35
37
  - `transcript` - print the transcript (local file, or post-call from Recall).
36
38
  - `screenshot` - capture the local Mac screen (fallback when no call frame is available).
37
39
  - `dicts` - list available Deepgram keyword dictionaries.
40
+ - `doctor` - check local prerequisites before joining a call.
38
41
 
39
42
  The agent still decides what to say, when to inspect a frame, and how to use the meeting context. samoagent is the local adapter that exposes those call capabilities.
40
43
 
package/dist/cli.js CHANGED
@@ -1,12 +1,100 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
13
+ var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
21
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
22
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
+ for (let key of __getOwnPropNames(mod))
24
+ if (!__hasOwnProp.call(to, key))
25
+ __defProp(to, key, {
26
+ get: __accessProp.bind(mod, key),
27
+ enumerable: true
28
+ });
29
+ if (canCache)
30
+ cache.set(mod, to);
31
+ return to;
32
+ };
33
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
+
35
+ // package.json
36
+ var require_package = __commonJS((exports, module) => {
37
+ module.exports = {
38
+ name: "samoagent",
39
+ version: "0.4.1",
40
+ description: "Let AI agents join Zoom and Google Meet calls as active participants.",
41
+ type: "module",
42
+ license: "Apache-2.0",
43
+ bin: {
44
+ samoagent: "dist/cli.js"
45
+ },
46
+ files: [
47
+ "dist/cli.js",
48
+ "dictionaries/",
49
+ "LICENSE",
50
+ "README.md",
51
+ "docs/",
52
+ "avatar.html",
53
+ "avatar.png",
54
+ "logo.svg"
55
+ ],
56
+ repository: {
57
+ type: "git",
58
+ url: "git+https://github.com/NikolayS/samoagent.git"
59
+ },
60
+ homepage: "https://samoagent.dev/",
61
+ bugs: {
62
+ url: "https://github.com/NikolayS/samoagent/issues"
63
+ },
64
+ keywords: [
65
+ "ai-agent",
66
+ "meetings",
67
+ "recall-ai",
68
+ "zoom",
69
+ "google-meet",
70
+ "transcript",
71
+ "websocket"
72
+ ],
73
+ engines: {
74
+ bun: ">=1.2.0"
75
+ },
76
+ publishConfig: {
77
+ access: "public"
78
+ },
79
+ scripts: {
80
+ samoagent: "bun src/cli.ts",
81
+ test: "bun test",
82
+ build: "tsc --noEmit && bun build src/cli.ts --target bun --outfile dist/cli.js && chmod +x dist/cli.js",
83
+ prepack: "bun run build"
84
+ },
85
+ devDependencies: {
86
+ "@types/bun": "latest",
87
+ typescript: "^5.9.3"
88
+ }
89
+ };
90
+ });
3
91
 
4
92
  // src/config.ts
5
93
  import { homedir } from "os";
6
94
  import { join, dirname } from "path";
7
95
  import { fileURLToPath } from "url";
8
96
  var RECALL_BASE = "https://us-east-1.recall.ai/api/v1";
9
- var AVATAR_URL = "https://nikolays.github.io/samoagent/avatar.html";
97
+ var AVATAR_URL = "https://samoagent.dev/avatar.html";
10
98
 
11
99
  class ExitError extends Error {
12
100
  code;
@@ -1153,10 +1241,8 @@ async function cmdFrame(args, deps = {}) {
1153
1241
  }
1154
1242
  }
1155
1243
  const raw = new Uint8Array(await resp2.arrayBuffer());
1244
+ writeFrameFiles(out, raw, metadata);
1156
1245
  const output = archive && !args.out ? archiveFrameBytes(String(state.video_frame_dir ?? dirname4(out)), raw, metadata) : out;
1157
- if (!(archive && !args.out)) {
1158
- writeFrameFiles(output, raw, metadata);
1159
- }
1160
1246
  process.stdout.write(resolve3(output) + `
1161
1247
  `);
1162
1248
  return;
@@ -1280,7 +1366,7 @@ function serve(port, transcriptPath, options = {}) {
1280
1366
  let payload = {};
1281
1367
  try {
1282
1368
  const body = await req.text();
1283
- if (body.length > WEBHOOK_MAX_BYTES) {
1369
+ if (new TextEncoder().encode(body).byteLength > WEBHOOK_MAX_BYTES) {
1284
1370
  return Response.json({ error: "payload too large" }, { status: 413 });
1285
1371
  }
1286
1372
  payload = body ? JSON.parse(body) : {};
@@ -1352,10 +1438,81 @@ async function cmdServe(args) {
1352
1438
  await new Promise(() => {});
1353
1439
  }
1354
1440
 
1441
+ // src/commands/doctor.ts
1442
+ import { existsSync as existsSync10 } from "fs";
1443
+ function commandVersion(command, args = ["--version"]) {
1444
+ try {
1445
+ const proc = Bun.spawnSync([command, ...args]);
1446
+ if (proc.exitCode !== 0) {
1447
+ const stderr = new TextDecoder().decode(proc.stderr).trim();
1448
+ return {
1449
+ ok: false,
1450
+ detail: stderr || `${command} --version exited ${proc.exitCode}`
1451
+ };
1452
+ }
1453
+ const stdout = new TextDecoder().decode(proc.stdout).trim();
1454
+ return {
1455
+ ok: true,
1456
+ detail: stdout.split(/\r?\n/)[0] ?? ""
1457
+ };
1458
+ } catch (e) {
1459
+ return {
1460
+ ok: false,
1461
+ detail: e instanceof Error ? e.message : String(e)
1462
+ };
1463
+ }
1464
+ }
1465
+ async function cmdDoctor() {
1466
+ const bunVersion = commandVersion("bun");
1467
+ const ngrokVersion = commandVersion("ngrok");
1468
+ const ffmpegVersion = commandVersion("ffmpeg", ["-version"]);
1469
+ const checks = [
1470
+ {
1471
+ name: "Bun",
1472
+ ok: bunVersion.ok,
1473
+ detail: bunVersion.detail || "not found in PATH"
1474
+ },
1475
+ {
1476
+ name: "RECALL_API_KEY",
1477
+ ok: Boolean(process.env.RECALL_API_KEY),
1478
+ detail: process.env.RECALL_API_KEY ? "set" : "missing"
1479
+ },
1480
+ {
1481
+ name: "ngrok",
1482
+ ok: ngrokVersion.ok,
1483
+ detail: ngrokVersion.detail || "not found in PATH"
1484
+ },
1485
+ {
1486
+ name: "ffmpeg",
1487
+ ok: ffmpegVersion.ok,
1488
+ detail: ffmpegVersion.detail || "not found in PATH"
1489
+ },
1490
+ {
1491
+ name: "state",
1492
+ ok: true,
1493
+ detail: existsSync10(stateFile()) ? `active state at ${stateFile()}` : "no active bot state"
1494
+ }
1495
+ ];
1496
+ process.stdout.write(`samoagent doctor
1497
+
1498
+ `);
1499
+ for (const check of checks) {
1500
+ process.stdout.write(`${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.detail}
1501
+ `);
1502
+ }
1503
+ if (checks.some((check) => !check.ok)) {
1504
+ process.exit(1);
1505
+ }
1506
+ }
1507
+
1355
1508
  // src/cli.ts
1356
1509
  var USAGE = `usage: samoagent <command> [options]
1357
1510
 
1358
- AI meeting agent for Zoom & Google Meet
1511
+ Put your AI agent in Zoom and Google Meet calls.
1512
+ samoagent joins through Recall.ai, streams live transcript lines,
1513
+ captures call frames on demand, and sends explicit chat messages.
1514
+
1515
+ Requires: Bun, RECALL_API_KEY env var (get one at recall.ai), and ngrok.
1359
1516
 
1360
1517
  commands:
1361
1518
  join <url> [--name N] [--dict D] [--port P] [--transcript-dir DIR] [--rtmp-url URL] [--rtmp] [--no-ws-video] [--frame-dir DIR]
@@ -1367,7 +1524,52 @@ commands:
1367
1524
  dicts
1368
1525
  watch
1369
1526
  frame [--out FILE] [--archive] [bot_id]
1527
+ doctor
1528
+
1529
+ flags:
1530
+ -h, --help Show this help message
1531
+ -v, --version Show version number
1370
1532
  `;
1533
+ var COMMAND_HELP = {
1534
+ join: `usage: samoagent join <url> [options]
1535
+
1536
+ Join a Zoom or Google Meet call as a Recall.ai bot.
1537
+ By default, samoagent streams transcript events and receives call frames over WebSocket.
1538
+
1539
+ options:
1540
+ --name N Bot display name
1541
+ --dict D Deepgram keyword dictionary name
1542
+ --port P Local callback server port (default: 8080)
1543
+ --transcript-dir DIR Directory for transcript.txt
1544
+ --frame-dir DIR Directory for on-demand frame output
1545
+ --no-ws-video Disable WebSocket call-frame capture
1546
+ --rtmp Use local RTMP path through ngrok TCP
1547
+ --rtmp-url URL Use an existing RTMP endpoint
1548
+
1549
+ examples:
1550
+ samoagent join "https://meet.google.com/abc-defg-hij" --name Leo
1551
+ samoagent join "https://zoom.us/j/123" --dict postgresfm
1552
+ `,
1553
+ frame: `usage: samoagent frame [--out FILE] [--archive] [bot_id]
1554
+
1555
+ Write the latest call frame to disk.
1556
+ With the default WebSocket path, frames stay in memory until this command is run.
1557
+
1558
+ options:
1559
+ --out FILE Output path. Defaults to latest frame path from active state.
1560
+ --archive Also write a timestamped PNG+JSON archive copy.
1561
+
1562
+ examples:
1563
+ samoagent frame
1564
+ samoagent frame --out /tmp/current-call.png
1565
+ samoagent frame --archive
1566
+ `,
1567
+ doctor: `usage: samoagent doctor
1568
+
1569
+ Check local prerequisites for joining meetings:
1570
+ Bun, RECALL_API_KEY, ngrok, ffmpeg, and active samoagent state.
1571
+ `
1572
+ };
1371
1573
 
1372
1574
  class ArgError extends Error {
1373
1575
  }
@@ -1389,6 +1591,7 @@ function parseArgs(argv) {
1389
1591
  dicts: new Set,
1390
1592
  watch: new Set,
1391
1593
  frame: new Set(["--out"]),
1594
+ doctor: new Set,
1392
1595
  _serve: new Set(["--port", "--transcript-file", "--webhook-token", "--call-id-file", "--frame-token"])
1393
1596
  };
1394
1597
  const boolFlags = {
@@ -1401,6 +1604,7 @@ function parseArgs(argv) {
1401
1604
  dicts: new Set,
1402
1605
  watch: new Set,
1403
1606
  frame: new Set(["--archive"]),
1607
+ doctor: new Set,
1404
1608
  _serve: new Set
1405
1609
  };
1406
1610
  const knownCommands = Object.keys(valueFlags);
@@ -1494,6 +1698,7 @@ function parseArgs(argv) {
1494
1698
  }
1495
1699
  case "dicts":
1496
1700
  case "watch":
1701
+ case "doctor":
1497
1702
  break;
1498
1703
  case "_serve": {
1499
1704
  const rawPort2 = opts["--port"];
@@ -1538,6 +1743,8 @@ async function dispatch(args) {
1538
1743
  return cmdDicts();
1539
1744
  case "watch":
1540
1745
  return cmdWatch();
1746
+ case "doctor":
1747
+ return cmdDoctor();
1541
1748
  case "_serve":
1542
1749
  return cmdServe(args);
1543
1750
  default:
@@ -1546,10 +1753,21 @@ async function dispatch(args) {
1546
1753
  }
1547
1754
  async function main() {
1548
1755
  const argv = process.argv.slice(2);
1549
- if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h" || argv.includes("--help") || argv.includes("-h")) {
1756
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
1550
1757
  process.stdout.write(USAGE);
1551
1758
  process.exit(argv.length === 0 ? 2 : 0);
1552
1759
  }
1760
+ if (argv.length >= 2 && (argv[1] === "--help" || argv[1] === "-h")) {
1761
+ const help = COMMAND_HELP[argv[0]];
1762
+ process.stdout.write(help ?? USAGE);
1763
+ process.exit(help ? 0 : 2);
1764
+ }
1765
+ if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "-V") {
1766
+ const pkg = await Promise.resolve().then(() => __toESM(require_package(), 1));
1767
+ process.stdout.write(`samoagent ${pkg.version}
1768
+ `);
1769
+ process.exit(0);
1770
+ }
1553
1771
  let args;
1554
1772
  try {
1555
1773
  args = parseArgs(argv);
@@ -0,0 +1,56 @@
1
+ # Release Checklist
2
+
3
+ Use this checklist for each npm release.
4
+
5
+ ## Before Release
6
+
7
+ - Confirm `main` is clean and up to date.
8
+ - Run `bun test`.
9
+ - Run `bun run build`.
10
+ - Smoke-test the built CLI:
11
+
12
+ ```bash
13
+ ./dist/cli.js --version
14
+ ./dist/cli.js --help
15
+ ```
16
+
17
+ - Check package metadata:
18
+
19
+ ```bash
20
+ npm pack --dry-run
21
+ ```
22
+
23
+ - Confirm `package.json` has the intended version, description, homepage, license, files, and keywords.
24
+ - Confirm `README.md` installation and usage examples match the current CLI.
25
+
26
+ ## Publish
27
+
28
+ - Bump `package.json` version.
29
+ - Commit and push the version bump.
30
+ - Create and publish a GitHub release tag matching the package version, for example `v0.4.1`.
31
+ - Wait for the `Publish to npm` GitHub Actions workflow to pass.
32
+
33
+ ## After Publish
34
+
35
+ - Verify npm has the new version:
36
+
37
+ ```bash
38
+ npm view samoagent version
39
+ ```
40
+
41
+ - Smoke-test the registry package from a clean prefix:
42
+
43
+ ```bash
44
+ tmp="$(mktemp -d)"
45
+ npm_config_prefix="$tmp" npm install -g samoagent
46
+ PATH="$tmp/bin:$PATH" samoagent --version
47
+ rm -rf "$tmp"
48
+ ```
49
+
50
+ - Confirm the package page shows Apache-2.0 license, homepage, README, and provenance.
51
+ - Confirm GitHub Pages is healthy at `https://samoagent.dev/`.
52
+
53
+ ## Secret Hygiene
54
+
55
+ - Keep `NPM_TOKEN` only in GitHub Actions secrets.
56
+ - Rotate `NPM_TOKEN` immediately if it is pasted into chat, logs, issues, PRs, or local shell history.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "samoagent",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Let AI agents join Zoom and Google Meet calls as active participants.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -12,7 +12,7 @@
12
12
  "dictionaries/",
13
13
  "LICENSE",
14
14
  "README.md",
15
- "CLAUDE.md",
15
+ "docs/",
16
16
  "avatar.html",
17
17
  "avatar.png",
18
18
  "logo.svg"
package/CLAUDE.md DELETED
@@ -1,67 +0,0 @@
1
- # samoagent Agent Notes
2
-
3
- Use samoagent to join a meeting, watch the live transcript, speak in meeting chat when asked, and capture the call view on demand.
4
-
5
- ## Preferred Flow
6
-
7
- ```bash
8
- samoagent join "https://meet.google.com/..." --name Leo --dict postgresfm
9
- samoagent watch
10
- samoagent frame
11
- samoagent leave
12
- ```
13
-
14
- Start `watch` immediately after `join` with your persistent monitor. Keep it running until the call ends. Each line is:
15
-
16
- ```text
17
- [timestamp] Speaker: utterance
18
- ```
19
-
20
- React in your agent session. Use meeting chat only for deliberate call-visible messages:
21
-
22
- ```bash
23
- samoagent chat "Short message to the meeting"
24
- ```
25
-
26
- ## Looking At The Call
27
-
28
- Frame capture is on by default. Recall sends `video_separate_png.data` frames over the ngrok HTTPS/WSS tunnel. Frames stay in server memory; disk writes happen only when the agent calls:
29
-
30
- ```bash
31
- samoagent frame
32
- ```
33
-
34
- Default output is outside the repo:
35
-
36
- ```text
37
- ~/.samoagent/frames/latest.png
38
- ~/.samoagent/frames/latest.json
39
- ```
40
-
41
- Use explicit outputs only when needed:
42
-
43
- ```bash
44
- samoagent frame --out /tmp/call.png
45
- samoagent frame --archive
46
- ```
47
-
48
- `--archive` creates a timestamped filename with bot id, source type, and participant id.
49
-
50
- ## Mixed Video
51
-
52
- Use RTMP only when separate PNG frames are not enough:
53
-
54
- ```bash
55
- samoagent join "https://zoom.us/j/..." --rtmp
56
- samoagent join "https://zoom.us/j/..." --rtmp-url rtmp://HOST:1935/live/call
57
- ```
58
-
59
- `--rtmp` needs ngrok TCP, which requires ngrok card verification. `--rtmp-url` needs a public RTMP receiver.
60
-
61
- ## End The Call
62
-
63
- ```bash
64
- samoagent leave
65
- ```
66
-
67
- `leave` removes the bot, stops local helper processes, writes the `SAMOAGENT_CALL_ENDED` sentinel, and lets `watch` exit cleanly.