@ouro.bot/cli 0.1.0-alpha.27 → 0.1.0-alpha.28
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/changelog.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.28",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Bare npx ouro.bot now stays aligned with the current alpha CLI track because the published ouro.bot wrapper is version-locked and republished alongside the CLI instead of lagging behind it.",
|
|
8
|
+
"Slugger no longer re-opens active iMessage task threads with generic greetings like 'hiya' when work is already in motion; fresh idle conversations can still start warmly.",
|
|
9
|
+
"BlueBubbles voice notes now use a harness-managed whisper.cpp transcription path for the current OpenAI, Anthropic, and MiniMax runtime contracts, including automatic local provisioning and truthful error notices when transcription cannot complete."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
4
12
|
{
|
|
5
13
|
"version": "0.1.0-alpha.27",
|
|
6
14
|
"changes": [
|
|
@@ -7,6 +7,26 @@ function wrapperPackageChanged(changedFiles) {
|
|
|
7
7
|
}
|
|
8
8
|
function assessWrapperPublishSync(input) {
|
|
9
9
|
let result;
|
|
10
|
+
if (input.localVersion !== input.cliVersion) {
|
|
11
|
+
result = {
|
|
12
|
+
ok: false,
|
|
13
|
+
message: `ouro.bot wrapper version ${input.localVersion} must match @ouro.bot/cli version ${input.cliVersion}`,
|
|
14
|
+
};
|
|
15
|
+
(0, runtime_1.emitNervesEvent)({
|
|
16
|
+
level: "warn",
|
|
17
|
+
component: "daemon",
|
|
18
|
+
event: "daemon.wrapper_publish_guard_checked",
|
|
19
|
+
message: "evaluated wrapper publish sync",
|
|
20
|
+
meta: {
|
|
21
|
+
changed: wrapperPackageChanged(input.changedFiles),
|
|
22
|
+
localVersion: input.localVersion,
|
|
23
|
+
cliVersion: input.cliVersion,
|
|
24
|
+
publishedVersion: input.publishedVersion,
|
|
25
|
+
ok: result.ok,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
10
30
|
if (!wrapperPackageChanged(input.changedFiles)) {
|
|
11
31
|
result = {
|
|
12
32
|
ok: true,
|
|
@@ -16,7 +36,13 @@ function assessWrapperPublishSync(input) {
|
|
|
16
36
|
component: "daemon",
|
|
17
37
|
event: "daemon.wrapper_publish_guard_checked",
|
|
18
38
|
message: "evaluated wrapper publish sync",
|
|
19
|
-
meta: {
|
|
39
|
+
meta: {
|
|
40
|
+
changed: false,
|
|
41
|
+
localVersion: input.localVersion,
|
|
42
|
+
cliVersion: input.cliVersion,
|
|
43
|
+
publishedVersion: input.publishedVersion,
|
|
44
|
+
ok: result.ok,
|
|
45
|
+
},
|
|
20
46
|
});
|
|
21
47
|
return result;
|
|
22
48
|
}
|
|
@@ -30,7 +56,13 @@ function assessWrapperPublishSync(input) {
|
|
|
30
56
|
component: "daemon",
|
|
31
57
|
event: "daemon.wrapper_publish_guard_checked",
|
|
32
58
|
message: "evaluated wrapper publish sync",
|
|
33
|
-
meta: {
|
|
59
|
+
meta: {
|
|
60
|
+
changed: true,
|
|
61
|
+
localVersion: input.localVersion,
|
|
62
|
+
cliVersion: input.cliVersion,
|
|
63
|
+
publishedVersion: input.publishedVersion,
|
|
64
|
+
ok: result.ok,
|
|
65
|
+
},
|
|
34
66
|
});
|
|
35
67
|
return result;
|
|
36
68
|
}
|
|
@@ -42,7 +74,13 @@ function assessWrapperPublishSync(input) {
|
|
|
42
74
|
component: "daemon",
|
|
43
75
|
event: "daemon.wrapper_publish_guard_checked",
|
|
44
76
|
message: "evaluated wrapper publish sync",
|
|
45
|
-
meta: {
|
|
77
|
+
meta: {
|
|
78
|
+
changed: true,
|
|
79
|
+
localVersion: input.localVersion,
|
|
80
|
+
cliVersion: input.cliVersion,
|
|
81
|
+
publishedVersion: input.publishedVersion,
|
|
82
|
+
ok: result.ok,
|
|
83
|
+
},
|
|
46
84
|
});
|
|
47
85
|
return result;
|
|
48
86
|
}
|
|
@@ -37,7 +37,8 @@ function getFirstImpressions(friend) {
|
|
|
37
37
|
lines.push("- what do they do outside of work that they care about?");
|
|
38
38
|
lines.push("i don't ask all of these at once -- i weave them into conversation naturally, one or two at a time, and i genuinely follow up on what they share.");
|
|
39
39
|
lines.push("i introduce what i can do -- i have tools, integrations, and skills that can help them. i mention these naturally as they become relevant.");
|
|
40
|
-
lines.push("if
|
|
40
|
+
lines.push("if we're already in motion on a task, thread, or follow-up, i do not reset with a generic opener like 'hiya' or 'what do ya need help with?'. i continue directly or ask the specific next question.");
|
|
41
|
+
lines.push("only when the conversation is genuinely fresh and idle, with no active ask or thread in flight, a light opener is okay.");
|
|
41
42
|
lines.push("i save everything i learn immediately with save_friend_note -- names, roles, preferences, projects, anything. the bar is low: if i learned it, i save it.");
|
|
42
43
|
return lines.join("\n");
|
|
43
44
|
}
|
|
@@ -196,7 +196,8 @@ function extractRepairData(payload) {
|
|
|
196
196
|
return asRecord(record?.data) ?? record;
|
|
197
197
|
}
|
|
198
198
|
function providerSupportsAudioInput(provider) {
|
|
199
|
-
|
|
199
|
+
void provider;
|
|
200
|
+
return false;
|
|
200
201
|
}
|
|
201
202
|
async function resolveChatGuid(chat, config, channelConfig) {
|
|
202
203
|
return chat.chatGuid
|
|
@@ -62,6 +62,12 @@ const AUDIO_INPUT_FORMAT_BY_EXTENSION = {
|
|
|
62
62
|
".wav": "wav",
|
|
63
63
|
".mp3": "mp3",
|
|
64
64
|
};
|
|
65
|
+
const WHISPER_CPP_FORMULA = "whisper-cpp";
|
|
66
|
+
const WHISPER_CPP_MODEL_NAME = "ggml-base.en.bin";
|
|
67
|
+
const WHISPER_CPP_MODEL_URL = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_CPP_MODEL_NAME}`;
|
|
68
|
+
const WHISPER_CPP_TOOLS_DIR = path.join(os.homedir(), ".agentstate", "tools", "whisper-cpp");
|
|
69
|
+
const WHISPER_CPP_MODELS_DIR = path.join(WHISPER_CPP_TOOLS_DIR, "models");
|
|
70
|
+
const WHISPER_CPP_MODEL_PATH = path.join(WHISPER_CPP_MODELS_DIR, WHISPER_CPP_MODEL_NAME);
|
|
65
71
|
function buildBlueBubblesApiUrl(baseUrl, endpoint, password) {
|
|
66
72
|
const root = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
67
73
|
const url = new URL(endpoint.replace(/^\//, ""), root);
|
|
@@ -107,36 +113,123 @@ function audioFormatForInput(contentType, attachment) {
|
|
|
107
113
|
const extension = path.extname(attachment?.transferName ?? "").toLowerCase();
|
|
108
114
|
return AUDIO_INPUT_FORMAT_BY_CONTENT_TYPE[contentType ?? ""] ?? AUDIO_INPUT_FORMAT_BY_EXTENSION[extension];
|
|
109
115
|
}
|
|
110
|
-
async function
|
|
116
|
+
async function execFileText(file, args, timeout) {
|
|
117
|
+
return await new Promise((resolve, reject) => {
|
|
118
|
+
(0, node_child_process_1.execFile)(file, args, { timeout }, (error, stdout = "", stderr = "") => {
|
|
119
|
+
if (error) {
|
|
120
|
+
const detail = stderr.trim() || stdout.trim() || error.message;
|
|
121
|
+
reject(new Error(detail));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
resolve(stdout);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async function pathExists(targetPath) {
|
|
129
|
+
try {
|
|
130
|
+
await fs.access(targetPath);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function resolveWhisperCppBinary(timeoutMs) {
|
|
138
|
+
try {
|
|
139
|
+
const existing = (await execFileText("which", ["whisper-cli"], timeoutMs)).trim();
|
|
140
|
+
if (existing) {
|
|
141
|
+
return existing;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// fall through to managed install
|
|
146
|
+
}
|
|
147
|
+
let prefix = "";
|
|
148
|
+
try {
|
|
149
|
+
prefix = (await execFileText("brew", ["--prefix", WHISPER_CPP_FORMULA], timeoutMs)).trim();
|
|
150
|
+
if (prefix) {
|
|
151
|
+
const candidate = path.join(prefix, "bin", "whisper-cli");
|
|
152
|
+
if (await pathExists(candidate)) {
|
|
153
|
+
return candidate;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// fall through to managed install
|
|
159
|
+
}
|
|
160
|
+
await execFileText("brew", ["install", WHISPER_CPP_FORMULA], Math.max(timeoutMs, 300_000));
|
|
161
|
+
prefix = (await execFileText("brew", ["--prefix", WHISPER_CPP_FORMULA], timeoutMs)).trim();
|
|
162
|
+
if (!prefix) {
|
|
163
|
+
throw new Error("whisper.cpp installed but brew did not return a usable prefix");
|
|
164
|
+
}
|
|
165
|
+
const candidate = path.join(prefix, "bin", "whisper-cli");
|
|
166
|
+
if (!await pathExists(candidate)) {
|
|
167
|
+
throw new Error("whisper.cpp installed but whisper-cli binary is missing");
|
|
168
|
+
}
|
|
169
|
+
return candidate;
|
|
170
|
+
}
|
|
171
|
+
async function ensureWhisperCppModel(timeoutMs, fetchImpl) {
|
|
172
|
+
try {
|
|
173
|
+
await fs.access(WHISPER_CPP_MODEL_PATH);
|
|
174
|
+
return WHISPER_CPP_MODEL_PATH;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
await fs.mkdir(WHISPER_CPP_MODELS_DIR, { recursive: true });
|
|
178
|
+
const response = await fetchImpl(WHISPER_CPP_MODEL_URL, {
|
|
179
|
+
method: "GET",
|
|
180
|
+
signal: AbortSignal.timeout(Math.max(timeoutMs, 300_000)),
|
|
181
|
+
});
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
throw new Error(`failed to download whisper.cpp model: HTTP ${response.status}`);
|
|
184
|
+
}
|
|
185
|
+
await fs.writeFile(WHISPER_CPP_MODEL_PATH, Buffer.from(await response.arrayBuffer()));
|
|
186
|
+
return WHISPER_CPP_MODEL_PATH;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function convertAudioForWhisperCpp(sourcePath, outputPath, timeoutMs) {
|
|
190
|
+
try {
|
|
191
|
+
await execFileText("ffmpeg", ["-y", "-i", sourcePath, "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", outputPath], Math.max(timeoutMs, 120_000));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
catch (ffmpegError) {
|
|
195
|
+
try {
|
|
196
|
+
await execFileText("afconvert", ["-f", "WAVE", "-d", "LEI16@16000", "-c", "1", sourcePath, outputPath], Math.max(timeoutMs, 120_000));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
catch (afconvertError) {
|
|
200
|
+
const ffmpegReason = ffmpegError.message;
|
|
201
|
+
const afconvertReason = afconvertError.message;
|
|
202
|
+
throw new Error(`failed to prepare audio for whisper.cpp (ffmpeg: ${ffmpegReason}; afconvert: ${afconvertReason})`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function transcribeAudioWithWhisperCpp(params, modelFetchImpl = fetch) {
|
|
111
207
|
const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "ouro-bb-audio-"));
|
|
112
208
|
const filename = sanitizeFilename(describeAttachment(params.attachment));
|
|
113
209
|
const extension = fileExtensionForAudio(params.attachment, params.contentType);
|
|
114
210
|
const audioPath = path.join(workDir, `${path.parse(filename).name}${extension}`);
|
|
211
|
+
const wavPath = path.join(workDir, `${path.parse(audioPath).name}.wav`);
|
|
212
|
+
const outputBase = path.join(workDir, path.parse(audioPath).name);
|
|
115
213
|
try {
|
|
116
214
|
await fs.writeFile(audioPath, params.buffer);
|
|
117
|
-
await
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"--output_dir",
|
|
123
|
-
workDir,
|
|
124
|
-
"--output_format",
|
|
125
|
-
"json",
|
|
126
|
-
"--verbose",
|
|
127
|
-
"False",
|
|
128
|
-
], { timeout: Math.max(params.timeoutMs, 120000) }, (error) => {
|
|
129
|
-
if (error) {
|
|
130
|
-
reject(error);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
resolve();
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
const transcriptPath = path.join(workDir, `${path.parse(audioPath).name}.json`);
|
|
215
|
+
const whisperCliPath = await resolveWhisperCppBinary(params.timeoutMs);
|
|
216
|
+
const modelPath = await ensureWhisperCppModel(params.timeoutMs, modelFetchImpl);
|
|
217
|
+
await convertAudioForWhisperCpp(audioPath, wavPath, params.timeoutMs);
|
|
218
|
+
await execFileText(whisperCliPath, ["-m", modelPath, "-f", wavPath, "-oj", "-of", outputBase], Math.max(params.timeoutMs, 120_000));
|
|
219
|
+
const transcriptPath = `${outputBase}.json`;
|
|
137
220
|
const raw = await fs.readFile(transcriptPath, "utf8");
|
|
138
221
|
const parsed = JSON.parse(raw);
|
|
139
|
-
|
|
222
|
+
if (typeof parsed.text === "string") {
|
|
223
|
+
return parsed.text.trim();
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(parsed.transcription)) {
|
|
226
|
+
return parsed.transcription
|
|
227
|
+
.map((entry) => (typeof entry?.text === "string" ? entry.text.trim() : ""))
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
.join(" ")
|
|
230
|
+
.trim();
|
|
231
|
+
}
|
|
232
|
+
return "";
|
|
140
233
|
}
|
|
141
234
|
finally {
|
|
142
235
|
await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
@@ -178,7 +271,8 @@ async function hydrateBlueBubblesAttachments(attachments, config, channelConfig,
|
|
|
178
271
|
},
|
|
179
272
|
});
|
|
180
273
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
181
|
-
const
|
|
274
|
+
const modelFetchImpl = deps.modelFetchImpl ?? fetch;
|
|
275
|
+
const transcribeAudio = deps.transcribeAudio ?? ((params) => transcribeAudioWithWhisperCpp(params, modelFetchImpl));
|
|
182
276
|
const preferAudioInput = deps.preferAudioInput ?? false;
|
|
183
277
|
const inputParts = [];
|
|
184
278
|
const transcriptAdditions = [];
|