@ouro.bot/cli 0.1.0-alpha.27 → 0.1.0-alpha.29

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,21 @@
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.29",
6
+ "changes": [
7
+ "Running `ouro up` now force-syncs the global `ouro.bot` wrapper so bare `npx ouro.bot` stops getting hijacked by stale global CLI bins and lands back on the intended latest runtime.",
8
+ "Bootstrap repair is now explicit about reclaiming the `ouro.bot` command from old global installs, which makes repeated bootstrap runs more trustworthy on machines with prior experimental installs."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.28",
13
+ "changes": [
14
+ "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.",
15
+ "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.",
16
+ "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."
17
+ ]
18
+ },
4
19
  {
5
20
  "version": "0.1.0-alpha.27",
6
21
  "changes": [
@@ -60,6 +60,7 @@ const agent_discovery_1 = require("./agent-discovery");
60
60
  const update_hooks_1 = require("./update-hooks");
61
61
  const bundle_meta_1 = require("./hooks/bundle-meta");
62
62
  const bundle_manifest_1 = require("../../mind/bundle-manifest");
63
+ const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
63
64
  function stringField(value) {
64
65
  return typeof value === "string" ? value : null;
65
66
  }
@@ -887,6 +888,7 @@ function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
887
888
  runAdoptionSpecialist: defaultRunAdoptionSpecialist,
888
889
  registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
889
890
  installOuroCommand: ouro_path_installer_1.installOuroCommand,
891
+ syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
890
892
  /* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
891
893
  startChat: async (agentName) => {
892
894
  const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
@@ -964,6 +966,20 @@ async function performSystemSetup(deps) {
964
966
  });
965
967
  }
966
968
  }
969
+ if (deps.syncGlobalOuroBotWrapper) {
970
+ try {
971
+ await Promise.resolve(deps.syncGlobalOuroBotWrapper());
972
+ }
973
+ catch (error) {
974
+ (0, runtime_1.emitNervesEvent)({
975
+ level: "warn",
976
+ component: "daemon",
977
+ event: "daemon.system_setup_ouro_bot_wrapper_error",
978
+ message: "failed to sync global ouro.bot wrapper",
979
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
980
+ });
981
+ }
982
+ }
967
983
  // Install subagents (claude/codex skills)
968
984
  try {
969
985
  await deps.installSubagents();
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.syncGlobalOuroBotWrapper = syncGlobalOuroBotWrapper;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const runtime_1 = require("../../nerves/runtime");
41
+ const runtime_metadata_1 = require("./runtime-metadata");
42
+ function normalizeOutput(output) {
43
+ return (typeof output === "string" ? output : output.toString("utf-8")).trim();
44
+ }
45
+ function resolveGlobalPrefix(execFileSyncImpl) {
46
+ return normalizeOutput(execFileSyncImpl("npm", ["prefix", "-g"], { encoding: "utf-8" }));
47
+ }
48
+ function resolveGlobalRoot(execFileSyncImpl) {
49
+ return normalizeOutput(execFileSyncImpl("npm", ["root", "-g"], { encoding: "utf-8" }));
50
+ }
51
+ function readInstalledWrapperVersion(globalRoot, existsSyncImpl, readFileSyncImpl) {
52
+ const packageJsonPath = path.join(globalRoot, "ouro.bot", "package.json");
53
+ if (!existsSyncImpl(packageJsonPath))
54
+ return null;
55
+ try {
56
+ const parsed = JSON.parse(readFileSyncImpl(packageJsonPath, "utf-8"));
57
+ return typeof parsed.version === "string" && parsed.version.trim().length > 0 ? parsed.version.trim() : null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function resolveExecutableOwner(globalPrefix, platform, existsSyncImpl, realpathSyncImpl) {
64
+ const binName = platform === "win32" ? "ouro.bot.cmd" : "ouro.bot";
65
+ const binPath = platform === "win32"
66
+ ? path.join(globalPrefix, binName)
67
+ : path.join(globalPrefix, "bin", binName);
68
+ if (!existsSyncImpl(binPath))
69
+ return null;
70
+ try {
71
+ const resolved = realpathSyncImpl(binPath);
72
+ if (resolved.includes(`${path.sep}node_modules${path.sep}ouro.bot${path.sep}`))
73
+ return "wrapper";
74
+ if (resolved.includes(`${path.sep}node_modules${path.sep}@ouro.bot${path.sep}cli${path.sep}`))
75
+ return "cli";
76
+ return "other";
77
+ }
78
+ catch {
79
+ return "unknown";
80
+ }
81
+ }
82
+ function syncGlobalOuroBotWrapper(deps = {}) {
83
+ /* v8 ignore start -- dependency-injection defaults are only exercised in the live runtime */
84
+ const execFileSyncImpl = deps.execFileSync ?? child_process_1.execFileSync;
85
+ const existsSyncImpl = deps.existsSync ?? fs.existsSync;
86
+ const readFileSyncImpl = deps.readFileSync ?? fs.readFileSync;
87
+ const realpathSyncImpl = deps.realpathSync ?? fs.realpathSync;
88
+ const runtimeVersion = deps.runtimeVersion ?? (0, runtime_metadata_1.getRuntimeMetadata)().version;
89
+ const platform = deps.platform ?? process.platform;
90
+ /* v8 ignore stop */
91
+ (0, runtime_1.emitNervesEvent)({
92
+ component: "daemon",
93
+ event: "daemon.ouro_bot_global_sync_start",
94
+ message: "checking global ouro.bot wrapper",
95
+ meta: { version: runtimeVersion },
96
+ });
97
+ const globalPrefix = resolveGlobalPrefix(execFileSyncImpl);
98
+ const globalRoot = resolveGlobalRoot(execFileSyncImpl);
99
+ const installedVersion = readInstalledWrapperVersion(globalRoot, existsSyncImpl, readFileSyncImpl);
100
+ const executableOwner = resolveExecutableOwner(globalPrefix, platform, existsSyncImpl, realpathSyncImpl);
101
+ if (executableOwner === "wrapper") {
102
+ (0, runtime_1.emitNervesEvent)({
103
+ component: "daemon",
104
+ event: "daemon.ouro_bot_global_sync_end",
105
+ message: "global ouro.bot wrapper already current",
106
+ meta: { version: runtimeVersion, installedVersion, executableOwner, installed: false },
107
+ });
108
+ return {
109
+ installed: false,
110
+ version: runtimeVersion,
111
+ installedVersion,
112
+ executableOwner,
113
+ };
114
+ }
115
+ execFileSyncImpl("npm", ["install", "-g", "--force", "ouro.bot@latest"], { stdio: "pipe", encoding: "utf-8" });
116
+ (0, runtime_1.emitNervesEvent)({
117
+ component: "daemon",
118
+ event: "daemon.ouro_bot_global_sync_end",
119
+ message: "global ouro.bot wrapper synced",
120
+ meta: { version: runtimeVersion, installedVersion, executableOwner, installed: true },
121
+ });
122
+ return {
123
+ installed: true,
124
+ version: runtimeVersion,
125
+ installedVersion,
126
+ executableOwner,
127
+ };
128
+ }
@@ -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.29",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",