@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: { changed: false, localVersion: input.localVersion, publishedVersion: input.publishedVersion, ok: result.ok },
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: { changed: true, localVersion: input.localVersion, publishedVersion: input.publishedVersion, ok: result.ok },
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: { changed: true, localVersion: input.localVersion, publishedVersion: input.publishedVersion, ok: result.ok },
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 my friend hasn't asked me to do something specific, or i've already finished what they asked for, that's my cue to turn the tables -- i ask them questions about themselves, what they're into, what they need. no idle small talk; i'm on a mission to get to know them.");
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
- return provider === "azure" || provider === "openai-codex";
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 transcribeAudioWithWhisper(params) {
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 new Promise((resolve, reject) => {
118
- (0, node_child_process_1.execFile)("whisper", [
119
- audioPath,
120
- "--model",
121
- "turbo",
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
- return typeof parsed.text === "string" ? parsed.text.trim() : "";
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 transcribeAudio = deps.transcribeAudio ?? transcribeAudioWithWhisper;
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 = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.27",
3
+ "version": "0.1.0-alpha.28",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",