@bill10/agent-007 0.12.2001 → 0.12.2003
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/.env.example +3 -0
- package/VERSION +1 -1
- package/bin/agent-007.js +1 -0
- package/package.json +1 -1
- package/server/owner.js +36 -6
- package/server/voice.js +14 -1
package/.env.example
CHANGED
|
@@ -65,6 +65,9 @@
|
|
|
65
65
|
# Settings > Accessibility > Spoken Content > System voice > Manage Voices),
|
|
66
66
|
# else say's default. A voice that is not installed is logged and skipped.
|
|
67
67
|
# SAY_VOICE=Ava (Premium)
|
|
68
|
+
# How fast it speaks, in words per minute (120-300). Unset: 205, a little faster
|
|
69
|
+
# than say's own 175.
|
|
70
|
+
# SAY_RATE=205
|
|
68
71
|
# Your voice notes are transcribed with whisper.cpp (brew install whisper-cpp).
|
|
69
72
|
# WHISPER_MODEL is the full path to a ggml model, e.g. ggml-base.en.bin;
|
|
70
73
|
# WHISPER_CPP_BIN only if whisper-cli is not on PATH. Unset: voice notes get a
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.12.2.
|
|
1
|
+
0.12.2.3
|
package/bin/agent-007.js
CHANGED
|
@@ -48,6 +48,7 @@ Settings (default in brackets):
|
|
|
48
48
|
voice (macOS say + ffmpeg) [mirror]
|
|
49
49
|
SAY_VOICE macOS voice for that, from say -v '?' [best
|
|
50
50
|
installed English Premium/Enhanced voice]
|
|
51
|
+
SAY_RATE How fast it speaks, words per minute, 120-300 [205]
|
|
51
52
|
WHISPER_MODEL whisper.cpp model file; your voice notes are
|
|
52
53
|
transcribed locally [off]
|
|
53
54
|
WHISPER_CPP_BIN whisper.cpp CLI if not on PATH [whisper-cli]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bill10/agent-007",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2003",
|
|
4
4
|
"description": "From web terminals for your coding agents to a self-running agent company: Claude Code and Codex in parallel git worktrees, a job board they pick work from, and one agent that runs the board from a goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/server/owner.js
CHANGED
|
@@ -16,7 +16,7 @@ import { CONFIG_DIR } from './state.js';
|
|
|
16
16
|
import { liveBillion } from './billion.js';
|
|
17
17
|
import { sendText } from './messages.js';
|
|
18
18
|
import {
|
|
19
|
-
chooseMode, voiceSetting, speechUnavailable, synthesize, sayVoice, whisperSetup, transcribe, MAX_NOTE_SECONDS, MAX_NOTE_BYTES,
|
|
19
|
+
chooseMode, voiceSetting, speechUnavailable, synthesize, sayVoice, sayRate, whisperSetup, transcribe, MAX_NOTE_SECONDS, MAX_NOTE_BYTES,
|
|
20
20
|
} from './voice.js';
|
|
21
21
|
|
|
22
22
|
export const MAX_NOTIFY_CHARS = 3000; // Telegram's own limit is 4096
|
|
@@ -59,11 +59,17 @@ async function call(method, params, { env = process.env, signal } = {}) {
|
|
|
59
59
|
...(form ? { body: params } : { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params) }),
|
|
60
60
|
signal,
|
|
61
61
|
});
|
|
62
|
-
body = await res.json().catch(() => ({ ok: false, description: `HTTP ${res.status}` }));
|
|
62
|
+
body = await res.json().catch(() => ({ ok: false, error_code: res.status, description: `HTTP ${res.status}` }));
|
|
63
63
|
} catch (err) {
|
|
64
|
-
|
|
64
|
+
// reason: a short label for the poll loop's offline line.
|
|
65
|
+
const reason = /ENOTFOUND|EAI_AGAIN/.test(err.cause?.code) ? 'DNS'
|
|
66
|
+
: err.name === 'TimeoutError' || /timeout/i.test(err.message) ? 'timeout' : 'no network';
|
|
67
|
+
throw Object.assign(new Error(redact(err.cause?.message || err.message, env)), { reason });
|
|
68
|
+
}
|
|
69
|
+
if (!body?.ok) {
|
|
70
|
+
const status = body?.error_code;
|
|
71
|
+
throw Object.assign(new Error(redact(body?.description || 'Telegram said no', env)), { status, reason: status ? `HTTP ${status}` : 'Telegram said no' });
|
|
65
72
|
}
|
|
66
|
-
if (!body?.ok) throw new Error(redact(body?.description || 'Telegram said no', env));
|
|
67
73
|
return body.result;
|
|
68
74
|
}
|
|
69
75
|
|
|
@@ -414,6 +420,16 @@ export async function pollOnce(offset, { broadcast, env = process.env, signal }
|
|
|
414
420
|
|
|
415
421
|
let stopper = null;
|
|
416
422
|
|
|
423
|
+
// Failures that retrying will not fix, and what the owner should do about them.
|
|
424
|
+
const POLL_HINTS = {
|
|
425
|
+
401: 'the bot token is wrong or revoked',
|
|
426
|
+
409: 'another process is polling this bot',
|
|
427
|
+
};
|
|
428
|
+
const OFFLINE_AFTER_MS = 60 * 1000;
|
|
429
|
+
const OFFLINE_AFTER_FAILURES = 3;
|
|
430
|
+
|
|
431
|
+
const since = ms => ms < 120e3 ? `${Math.round(ms / 1000)}s` : `${Math.round(ms / 60e3)}m`;
|
|
432
|
+
|
|
417
433
|
const pause = (ms, signal) => new Promise(resolve => {
|
|
418
434
|
const timer = setTimeout(resolve, ms);
|
|
419
435
|
signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
@@ -425,20 +441,34 @@ export function startTelegram({ broadcast, env = process.env } = {}) {
|
|
|
425
441
|
if (!chatId) console.log(' Telegram: send any message to your bot, then set TELEGRAM_CHAT_ID=<id> (the id is shown here when it arrives)');
|
|
426
442
|
else console.log(' Telegram: on');
|
|
427
443
|
if (voiceSetting(env) !== 'never' && !speechUnavailable(env)) {
|
|
428
|
-
sayVoice(env).then(v => console.log(` Telegram: speaking with ${v ? `the ${v} voice` : "say's default voice"}`));
|
|
444
|
+
sayVoice(env).then(v => console.log(` Telegram: speaking with ${v ? `the ${v} voice` : "say's default voice"} at ${sayRate(env)} wpm`));
|
|
429
445
|
}
|
|
430
446
|
const controller = new AbortController();
|
|
431
447
|
stopper = controller;
|
|
432
448
|
(async () => {
|
|
433
449
|
let offset = 0;
|
|
434
450
|
let backoff = 1000;
|
|
451
|
+
// Sleep, wake and network changes fail a poll or two; log only going
|
|
452
|
+
// offline and coming back, not every retry.
|
|
453
|
+
let failures = 0, failingSince = 0, offline = false;
|
|
435
454
|
while (!controller.signal.aborted) {
|
|
436
455
|
try {
|
|
437
456
|
offset = await pollOnce(offset, { broadcast, env, signal: controller.signal });
|
|
457
|
+
if (offline) console.log(`Telegram: back online after ${since(Date.now() - failingSince)}`);
|
|
458
|
+
failures = 0;
|
|
459
|
+
offline = false;
|
|
438
460
|
backoff = 1000;
|
|
439
461
|
} catch (err) {
|
|
440
462
|
if (controller.signal.aborted) break;
|
|
441
|
-
|
|
463
|
+
if (!failures++) failingSince = Date.now();
|
|
464
|
+
const hint = POLL_HINTS[err.status];
|
|
465
|
+
// A hint logs even mid-outage: a token revoked while offline still needs saying.
|
|
466
|
+
if (hint ? offline !== hint : !offline && (failures >= OFFLINE_AFTER_FAILURES || Date.now() - failingSince > OFFLINE_AFTER_MS)) {
|
|
467
|
+
offline = hint || true;
|
|
468
|
+
console.error(hint
|
|
469
|
+
? `Telegram: getUpdates failed (${err.message}): ${hint}; retrying quietly`
|
|
470
|
+
: `Telegram: offline (${err.reason || 'no network'}), retrying quietly`);
|
|
471
|
+
}
|
|
442
472
|
await pause(backoff, controller.signal);
|
|
443
473
|
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
444
474
|
}
|
package/server/voice.js
CHANGED
|
@@ -120,13 +120,26 @@ export function sayVoice(env = process.env) {
|
|
|
120
120
|
return voicePick;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// SAY_RATE in words per minute, 120–300. Unset: 205, about 15% faster than
|
|
124
|
+
// say's own 175, which the owner found a little slow on Ava (Premium).
|
|
125
|
+
export const DEFAULT_SAY_RATE = 205;
|
|
126
|
+
let ratePick;
|
|
127
|
+
export function sayRate(env = process.env) {
|
|
128
|
+
if (ratePick) return ratePick;
|
|
129
|
+
const raw = (env.SAY_RATE || '').trim();
|
|
130
|
+
const n = Number(raw);
|
|
131
|
+
if (raw && !Number.isInteger(n)) console.log(` Telegram: SAY_RATE "${raw}" is not a whole number of words per minute; using ${DEFAULT_SAY_RATE}`);
|
|
132
|
+
ratePick = raw && Number.isInteger(n) ? Math.min(300, Math.max(120, n)) : DEFAULT_SAY_RATE;
|
|
133
|
+
return ratePick;
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
// text → OGG/Opus bytes. URLs are said as "link"; the caption carries them.
|
|
124
137
|
export function synthesize(text, env = process.env) {
|
|
125
138
|
return inTempDir(async dir => {
|
|
126
139
|
const txt = join(dir, 'say.txt'), aiff = join(dir, 'say.aiff'), ogg = join(dir, 'say.ogg');
|
|
127
140
|
await writeFile(txt, text.replace(URL_RE, 'link'));
|
|
128
141
|
const voice = await sayVoice(env);
|
|
129
|
-
await run('say', [...(voice ? ['-v', voice] : []), '-o', aiff, '-f', txt]);
|
|
142
|
+
await run('say', [...(voice ? ['-v', voice] : []), '-o', aiff, '-r', String(sayRate(env)), '-f', txt]);
|
|
130
143
|
await run('ffmpeg', ['-y', '-loglevel', 'error', '-protocol_whitelist', 'file', '-i', aiff, '-c:a', 'libopus', '-b:a', '32k', ogg]);
|
|
131
144
|
return readFile(ogg);
|
|
132
145
|
});
|