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 +6 -3
- package/dist/cli.js +225 -7
- package/docs/release-checklist.md +56 -0
- package/package.json +2 -2
- package/CLAUDE.md +0 -67
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
|
-
|
|
20
|
+
npm install -g samoagent
|
|
19
21
|
export RECALL_API_KEY=...
|
|
20
|
-
|
|
22
|
+
samoagent join "https://meet.google.com/..." --name Leo
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
During development use `bun
|
|
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://
|
|
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.
|
|
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
|
|
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"
|
|
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
|
+
"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
|
-
"
|
|
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.
|