alvin-bot 4.22.2 → 4.23.0
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.md +39 -0
- package/README.md +2 -0
- package/bin/cli.js +355 -1
- package/dist/providers/codex-cli-provider.js +6 -0
- package/install.sh +101 -12
- package/package.json +1 -1
- package/02_yandex.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [4.23.0] — 2026-05-12
|
|
6
|
+
|
|
7
|
+
### Installer: bootstrap Node automatically + optional capability tools
|
|
8
|
+
|
|
9
|
+
The first-time install path no longer assumes Node (or Homebrew, or anything beyond a normal Mac/Linux shell) is already in place, and the setup wizard now offers to install a curated set of universally useful CLIs in one step.
|
|
10
|
+
|
|
11
|
+
- **`install.sh` — auto-bootstraps Node.** New `ensure_brew()` + `ensure_node()` helpers replace the old "fail with manual install instructions" behaviour. On macOS, if Homebrew is missing, the installer offers to install it via the official `curl|bash` (with `NONINTERACTIVE=1` so it doesn't pause for ENTER). On Debian/Ubuntu, offers Node 22 via the NodeSource repo (with sudo confirmation). Non-interactive shells fall back to clear manual-install messages. Skipping the prompts is always an option. `eval "$(brew shellenv)"` is invoked after a fresh brew install so the rest of the script sees brew on PATH.
|
|
12
|
+
|
|
13
|
+
- **`alvin-bot setup` — optional tools step.** After Telegram + AI provider config, the wizard now lists eight commonly useful tools and lets the user install just the ones they want with a comma-separated picklist (or `a` for all missing, `n` to skip). Already-installed tools are marked `✓` and skipped automatically. Install failures don't block bot setup — the user gets a working bot regardless.
|
|
14
|
+
|
|
15
|
+
- **New `alvin-bot tools` command.** Re-runnable later: `alvin-bot tools list` shows what's installed, `alvin-bot tools install` opens the same interactive menu the setup wizard uses.
|
|
16
|
+
|
|
17
|
+
Tools offered (curated, all generic — none require maintainer-specific creds or workflows):
|
|
18
|
+
|
|
19
|
+
| Tool | What it unlocks |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Playwright + Chromium | Tier 1 stealth browser automation (web-research, social-fetch, browser-manager) |
|
|
22
|
+
| agent-browser CLI | Tier 1.5 token-efficient web automation (Vercel Labs) |
|
|
23
|
+
| ffmpeg | Audio/video processing for media-transcribe, video-generate, voice features |
|
|
24
|
+
| ImageMagick | Image conversion & manipulation for image-generate and visual skills |
|
|
25
|
+
| Pandoc | Markdown ↔ PDF/DOCX/HTML conversion for document-creation skill |
|
|
26
|
+
| ripgrep | Fast file/code search (`alvin-bot search`, code-aware skills) |
|
|
27
|
+
| jq | JSON parsing for helper scripts |
|
|
28
|
+
| himalaya | Multi-account IMAP/SMTP email CLI (configure after install) |
|
|
29
|
+
|
|
30
|
+
## [4.22.3] — 2026-05-12
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Codex CLI provider:** close stdin after spawn so `codex exec` returns immediately. The provider opened stdin as a pipe but never sent EOF — `codex exec` then printed `Reading additional input from stdin...` and blocked until the 120 s spawn timeout, which surfaced on the chat side as "no reply" / empty Telegram messages whenever a user picked Codex CLI as their AI provider. Single-line fix (`proc.stdin.end()`) plus an explanatory comment. ([#1](https://github.com/alvbln/Alvin-Bot/pull/1))
|
|
35
|
+
|
|
36
|
+
### macOS UX: surface Full Disk Access gaps under launchd ([#2](https://github.com/alvbln/Alvin-Bot/pull/2))
|
|
37
|
+
|
|
38
|
+
When the bot runs as a LaunchAgent, macOS TCC binds permissions to the `node` binary's real Cellar path. Anything the bot spawns (Codex CLI, file-reading skills, plugins touching `~/Documents`/`~/Desktop`) inherits that identity, and without Full Disk Access on `node` those reads silently fail — no dialog appears under launchd. Fresh public users were hitting this without knowing what was going on; the failure mode (spawned tools producing empty output) looked like a bot bug.
|
|
39
|
+
|
|
40
|
+
- **`alvin-bot launchd install`** now detects FDA after a successful install and either confirms it's granted or prints a prominent warning with: the exact Cellar path to add (resolved via `realpathSync`), the `open "x-apple.systempreferences:..."` command for the right pane, the `launchctl kickstart` command to apply the grant, and a heads-up that `brew upgrade node` invalidates the grant (TCC binds to the versioned Cellar path).
|
|
41
|
+
- **`alvin-bot doctor`** gains a "macOS permissions" section that shows FDA status and how to fix — useful after `brew upgrade node` rolls forward to a new Cellar path and the old grant becomes stale.
|
|
42
|
+
- **README** "A note on permission prompts" is extended with the launchd/FDA caveat, linking to the macOS Setup Guide PDF.
|
|
43
|
+
|
|
5
44
|
## [4.22.2] — 2026-05-11
|
|
6
45
|
|
|
7
46
|
### Docs
|
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ Free AI providers available — no credit card needed. **Privacy-first?** Pick t
|
|
|
68
68
|
|
|
69
69
|
The first time Alvin reaches for a new tool — a shell command, a file read, a web fetch — you may see a permission prompt from the underlying agent runtime asking whether to allow it. Those prompts come from Alvin himself, not from a third party. Approving one expands what he can do for you autonomously; denying keeps the scope narrow. The more you allow, the more capable and hands-off he becomes — you stay in control either way, and you can always revoke a permission later.
|
|
70
70
|
|
|
71
|
+
**macOS only — one extra step under launchd.** If you install Alvin as a background service (`alvin-bot launchd install`), macOS won't be able to show you those permission dialogs interactively anymore. To let the bot and anything it spawns (Codex CLI, file-reading skills) actually read your files, grant **Full Disk Access** to `node` once: System Settings → Privacy & Security → Full Disk Access → **+** → add `/opt/homebrew/Cellar/node/<version>/bin/node` (find the exact path with `readlink -f "$(which node)"`). `alvin-bot launchd install` and `alvin-bot doctor` will both detect and remind you with the exact path. After `brew upgrade node` you'll need to re-grant, because TCC binds to the versioned Cellar path. The printable [macOS Setup Guide PDF](https://github.com/alvbln/Alvin-Bot/releases/latest/download/Alvin-Bot-macOS-Setup-Guide.pdf) covers this end-to-end.
|
|
72
|
+
|
|
71
73
|
### 📘 First-time setup walkthroughs
|
|
72
74
|
|
|
73
75
|
Step-by-step printable PDF guides:
|
package/bin/cli.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { createInterface } from "readline";
|
|
20
|
-
import { existsSync, writeFileSync, readFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
20
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync, copyFileSync, readdirSync, statSync, accessSync, realpathSync, constants as fsConstants } from "fs";
|
|
21
21
|
import { resolve, join } from "path";
|
|
22
22
|
import { homedir } from "os";
|
|
23
23
|
import { execSync } from "child_process";
|
|
@@ -128,6 +128,237 @@ const PROVIDERS = [
|
|
|
128
128
|
},
|
|
129
129
|
];
|
|
130
130
|
|
|
131
|
+
// ── Optional capability tools ───────────────────────────────────────────────
|
|
132
|
+
//
|
|
133
|
+
// Curated, universally useful CLIs that significantly expand what skills can
|
|
134
|
+
// do. All optional, all opt-in. The list is intentionally short and generic —
|
|
135
|
+
// no maintainer-specific credentials or workflows are required to make any of
|
|
136
|
+
// these useful to a public user; auth/config is per-user and out of scope here.
|
|
137
|
+
//
|
|
138
|
+
// Each entry has:
|
|
139
|
+
// id machine-readable key
|
|
140
|
+
// name shown to the user
|
|
141
|
+
// desc one-liner explaining what skills/features it unlocks
|
|
142
|
+
// check() returns boolean — is it already installed?
|
|
143
|
+
// install either a shell-command string (single platform) or an object
|
|
144
|
+
// { macos, linux } with platform-specific shell commands, or a
|
|
145
|
+
// function that does whatever it takes
|
|
146
|
+
// skipOnLinuxIf optional: skip the Linux install if this command is missing
|
|
147
|
+
// (e.g. himalaya on Linux needs cargo)
|
|
148
|
+
const OPTIONAL_TOOLS = [
|
|
149
|
+
{
|
|
150
|
+
id: "playwright",
|
|
151
|
+
name: "Playwright + Chromium",
|
|
152
|
+
desc: "Browser automation (Tier 1 stealth) — used by web-research, social-fetch, browser-manager",
|
|
153
|
+
check: () => hasCommand("playwright") || hasCommand("npx") && tryNpx("playwright --version"),
|
|
154
|
+
install: () => {
|
|
155
|
+
execSync("npm install -g playwright", { stdio: "inherit" });
|
|
156
|
+
execSync("npx playwright install chromium", { stdio: "inherit" });
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "agent-browser",
|
|
161
|
+
name: "agent-browser CLI (Vercel Labs)",
|
|
162
|
+
desc: "Tier 1.5 token-efficient web automation — ~90% cheaper than raw Playwright for AI-driven tasks",
|
|
163
|
+
check: () => hasCommand("agent-browser"),
|
|
164
|
+
install: () => {
|
|
165
|
+
execSync("npm install -g agent-browser", { stdio: "inherit" });
|
|
166
|
+
// Best-effort: ignore failure if the install subcommand isn't there
|
|
167
|
+
try { execSync("agent-browser install", { stdio: "inherit" }); } catch {}
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "ffmpeg",
|
|
172
|
+
name: "ffmpeg",
|
|
173
|
+
desc: "Audio/video processing — required by media-transcribe, video-generate, voice features",
|
|
174
|
+
check: () => hasCommand("ffmpeg"),
|
|
175
|
+
install: { macos: "brew install ffmpeg", linux: "sudo apt-get install -y ffmpeg" },
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "imagemagick",
|
|
179
|
+
name: "ImageMagick",
|
|
180
|
+
desc: "Image conversion & manipulation — used by image-generate and visual skills",
|
|
181
|
+
check: () => hasCommand("magick") || hasCommand("convert"),
|
|
182
|
+
install: { macos: "brew install imagemagick", linux: "sudo apt-get install -y imagemagick" },
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "pandoc",
|
|
186
|
+
name: "Pandoc",
|
|
187
|
+
desc: "Document conversion (Markdown ↔ PDF/DOCX/HTML) — used by document-creation skill",
|
|
188
|
+
check: () => hasCommand("pandoc"),
|
|
189
|
+
install: { macos: "brew install pandoc", linux: "sudo apt-get install -y pandoc" },
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "ripgrep",
|
|
193
|
+
name: "ripgrep (rg)",
|
|
194
|
+
desc: "Fast file & code search — used by `alvin-bot search` and code-aware skills",
|
|
195
|
+
check: () => hasCommand("rg"),
|
|
196
|
+
install: { macos: "brew install ripgrep", linux: "sudo apt-get install -y ripgrep" },
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "jq",
|
|
200
|
+
name: "jq",
|
|
201
|
+
desc: "JSON parsing for shell scripts — used by many helper scripts and integrations",
|
|
202
|
+
check: () => hasCommand("jq"),
|
|
203
|
+
install: { macos: "brew install jq", linux: "sudo apt-get install -y jq" },
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "himalaya",
|
|
207
|
+
name: "himalaya (email CLI)",
|
|
208
|
+
desc: "Multi-account IMAP/SMTP email CLI — bring your own email accounts, configure after install",
|
|
209
|
+
check: () => hasCommand("himalaya"),
|
|
210
|
+
install: { macos: "brew install himalaya", linux: "cargo install himalaya" },
|
|
211
|
+
skipOnLinuxIf: "cargo",
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
function hasCommand(name) {
|
|
216
|
+
try {
|
|
217
|
+
execSync(`command -v ${name}`, { stdio: "pipe" });
|
|
218
|
+
return true;
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function tryNpx(args) {
|
|
225
|
+
try {
|
|
226
|
+
execSync(`npx --no-install ${args}`, { stdio: "pipe", timeout: 5000 });
|
|
227
|
+
return true;
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Make sure `brew` is reachable in the current process. The setup wizard
|
|
235
|
+
* may be running in a fresh shell right after `install.sh` installed brew
|
|
236
|
+
* for the first time — in that case `/opt/homebrew/bin` may not be in
|
|
237
|
+
* PATH yet. Side-loads it if we find the binary.
|
|
238
|
+
*/
|
|
239
|
+
function ensureBrewOnPath() {
|
|
240
|
+
if (hasCommand("brew")) return true;
|
|
241
|
+
for (const candidate of ["/opt/homebrew/bin", "/usr/local/bin"]) {
|
|
242
|
+
if (existsSync(join(candidate, "brew"))) {
|
|
243
|
+
process.env.PATH = `${candidate}:${process.env.PATH || ""}`;
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Print the status of optional tools without prompting. Used by
|
|
252
|
+
* `alvin-bot tools list` for inspection and scripting.
|
|
253
|
+
*/
|
|
254
|
+
function listOptionalTools() {
|
|
255
|
+
const platform = process.platform === "darwin" ? "macos"
|
|
256
|
+
: process.platform === "linux" ? "linux"
|
|
257
|
+
: null;
|
|
258
|
+
console.log(`\n📦 Optional capability tools — ${platform || process.platform}\n`);
|
|
259
|
+
const states = OPTIONAL_TOOLS.map(t => ({ ...t, installed: t.check() }));
|
|
260
|
+
states.forEach((t, i) => {
|
|
261
|
+
const mark = t.installed ? "✓" : "·";
|
|
262
|
+
console.log(` [${String(i + 1).padStart(2)}] ${mark} ${t.name}`);
|
|
263
|
+
console.log(` ${t.desc}`);
|
|
264
|
+
});
|
|
265
|
+
const missing = states.filter(t => !t.installed).length;
|
|
266
|
+
console.log(`\n ${states.length - missing} installed, ${missing} missing.`);
|
|
267
|
+
if (missing > 0) {
|
|
268
|
+
console.log(` Install missing with: alvin-bot tools install\n`);
|
|
269
|
+
} else {
|
|
270
|
+
console.log("");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function runOptionalToolsStep() {
|
|
275
|
+
// Skip on platforms we don't have install commands for.
|
|
276
|
+
const platform = process.platform === "darwin" ? "macos"
|
|
277
|
+
: process.platform === "linux" ? "linux"
|
|
278
|
+
: null;
|
|
279
|
+
if (!platform) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// On macOS, anything that uses brew needs brew on PATH.
|
|
284
|
+
if (platform === "macos") {
|
|
285
|
+
if (!ensureBrewOnPath()) {
|
|
286
|
+
console.log("\n ℹ️ Skipping optional tools step — Homebrew not found.");
|
|
287
|
+
console.log(" Install brew from https://brew.sh and re-run 'alvin-bot setup' to revisit.");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log(`\n━━━ Optional capability tools ━━━\n`);
|
|
293
|
+
console.log("These CLIs significantly expand what skills can do. All optional — skip");
|
|
294
|
+
console.log("now and run 'alvin-bot setup' later to revisit. Already-installed tools");
|
|
295
|
+
console.log("are skipped automatically.\n");
|
|
296
|
+
|
|
297
|
+
const states = OPTIONAL_TOOLS.map(t => ({ ...t, installed: t.check() }));
|
|
298
|
+
states.forEach((t, i) => {
|
|
299
|
+
const mark = t.installed ? "✓" : " ";
|
|
300
|
+
console.log(` [${String(i + 1).padStart(2)}] ${mark} ${t.name}`);
|
|
301
|
+
console.log(` ${t.desc}`);
|
|
302
|
+
});
|
|
303
|
+
console.log(`\n ✓ = already installed (will be skipped)\n`);
|
|
304
|
+
|
|
305
|
+
const ans = (await ask(" Install which? Comma-separated numbers, [a]ll missing, [n]one [n]: ")).trim().toLowerCase();
|
|
306
|
+
|
|
307
|
+
let selected = [];
|
|
308
|
+
if (ans === "" || ans === "n" || ans === "none") {
|
|
309
|
+
console.log(" Skipped — no tools installed this round.\n");
|
|
310
|
+
return;
|
|
311
|
+
} else if (ans === "a" || ans === "all") {
|
|
312
|
+
selected = states.filter(t => !t.installed);
|
|
313
|
+
} else {
|
|
314
|
+
const indices = ans.split(",").map(s => parseInt(s.trim(), 10)).filter(n => Number.isFinite(n));
|
|
315
|
+
selected = indices.map(n => states[n - 1]).filter(t => t && !t.installed);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (selected.length === 0) {
|
|
319
|
+
console.log(" Nothing to install — every pick was either invalid or already present.\n");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const installed = [];
|
|
324
|
+
const failed = [];
|
|
325
|
+
for (const tool of selected) {
|
|
326
|
+
// Linux-only fallbacks: skip if a hard prereq is missing.
|
|
327
|
+
if (platform === "linux" && tool.skipOnLinuxIf && !hasCommand(tool.skipOnLinuxIf)) {
|
|
328
|
+
console.log(`\n ⚠️ Skipping ${tool.name} — needs '${tool.skipOnLinuxIf}' which isn't installed.`);
|
|
329
|
+
failed.push({ tool, reason: `missing prerequisite: ${tool.skipOnLinuxIf}` });
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
console.log(`\n📦 Installing ${tool.name}...`);
|
|
333
|
+
try {
|
|
334
|
+
if (typeof tool.install === "function") {
|
|
335
|
+
tool.install();
|
|
336
|
+
} else if (typeof tool.install === "object" && tool.install[platform]) {
|
|
337
|
+
execSync(tool.install[platform], { stdio: "inherit" });
|
|
338
|
+
} else {
|
|
339
|
+
console.log(` ⚠️ No install command for ${platform}. Skipping.`);
|
|
340
|
+
failed.push({ tool, reason: `no install method for ${platform}` });
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
installed.push(tool);
|
|
344
|
+
console.log(` ✅ ${tool.name} installed.`);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
const msg = err && err.message ? err.message : String(err);
|
|
347
|
+
console.log(` ❌ ${tool.name} install failed: ${msg.split("\n")[0]}`);
|
|
348
|
+
console.log(` Bot setup will continue — retry this tool later with its native install command.`);
|
|
349
|
+
failed.push({ tool, reason: "install command failed" });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log("");
|
|
354
|
+
if (installed.length) {
|
|
355
|
+
console.log(` ✅ Installed: ${installed.map(t => t.name).join(", ")}`);
|
|
356
|
+
}
|
|
357
|
+
if (failed.length) {
|
|
358
|
+
console.log(` ⚠️ Skipped/failed: ${failed.map(f => f.tool.name).join(", ")}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
131
362
|
// ── Offline mode: Ollama + Gemma 4 E4B ─────────────────────────────────────
|
|
132
363
|
|
|
133
364
|
/**
|
|
@@ -1197,6 +1428,11 @@ async function setup() {
|
|
|
1197
1428
|
console.log(" ✅ CLAUDE.md initialized from example");
|
|
1198
1429
|
}
|
|
1199
1430
|
|
|
1431
|
+
// ── Optional capability tools (curated list of useful CLIs) ──────────────
|
|
1432
|
+
// Offered after the core setup (Telegram + AI provider) is complete so the
|
|
1433
|
+
// user always gets a working bot first — tool installs are pure upside.
|
|
1434
|
+
await runOptionalToolsStep();
|
|
1435
|
+
|
|
1200
1436
|
// ── Build (only for local/dev installs — global npm installs already have dist/)
|
|
1201
1437
|
const isGlobalInstall = !existsSync(resolve(process.cwd(), "tsconfig.json"));
|
|
1202
1438
|
if (!isGlobalInstall) {
|
|
@@ -1515,6 +1751,21 @@ async function doctor() {
|
|
|
1515
1751
|
console.log(` ${hasChrome ? "✅" : "⚠️ "} WhatsApp (Chrome: ${hasChrome ? "found" : "not found"})`);
|
|
1516
1752
|
}
|
|
1517
1753
|
|
|
1754
|
+
// ── macOS permissions (TCC) ────────────────────────────────────────────
|
|
1755
|
+
if (process.platform === "darwin") {
|
|
1756
|
+
console.log("\n macOS permissions:");
|
|
1757
|
+
const fda = checkMacosFullDiskAccess(process.execPath);
|
|
1758
|
+
if (fda.hasFDA) {
|
|
1759
|
+
console.log(` ✅ Full Disk Access — granted to ${fda.realNodePath}`);
|
|
1760
|
+
} else {
|
|
1761
|
+
console.log(` ⚠️ Full Disk Access NOT granted to node (${fda.realNodePath})`);
|
|
1762
|
+
console.log(` Spawned tools (codex CLI, file-reading skills) may silently fail`);
|
|
1763
|
+
console.log(` to read protected directories under launchd. To fix:`);
|
|
1764
|
+
console.log(` open "${fda.paneUrl}"`);
|
|
1765
|
+
console.log(` and add the path above with the "+" button.`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1518
1769
|
console.log("");
|
|
1519
1770
|
}
|
|
1520
1771
|
|
|
@@ -1557,6 +1808,57 @@ async function version() {
|
|
|
1557
1808
|
|
|
1558
1809
|
// ── LaunchAgent helpers (macOS only) ────────────────────────────────────────
|
|
1559
1810
|
|
|
1811
|
+
/**
|
|
1812
|
+
* Inspect the macOS Full Disk Access (TCC) state for the node binary that
|
|
1813
|
+
* the bot will run under.
|
|
1814
|
+
*
|
|
1815
|
+
* Background: macOS TCC grants permissions to specific binary paths, not to
|
|
1816
|
+
* user accounts. When `alvin-bot` runs under launchd, the bot process is
|
|
1817
|
+
* `node` from Homebrew (e.g. /opt/homebrew/Cellar/node/<version>/bin/node).
|
|
1818
|
+
* Any child process the bot spawns — `codex`, `ripgrep`, plugins reading
|
|
1819
|
+
* ~/Documents, the file-manager skill — inherits the parent's TCC identity.
|
|
1820
|
+
* If `node` doesn't have Full Disk Access, those children silently fail to
|
|
1821
|
+
* read protected directories (no dialog appears under launchd).
|
|
1822
|
+
*
|
|
1823
|
+
* Heuristic for "FDA granted": try to read `~/Library/Mail`, which lives in
|
|
1824
|
+
* the TCC-protected user-data quarantine. Successful read ⇒ FDA is on.
|
|
1825
|
+
* This matches `src/services/sudo.ts:getSudoStatus()`.
|
|
1826
|
+
*
|
|
1827
|
+
* Returns { realNodePath, hasFDA, panEUrl, settingsCommand } so callers can
|
|
1828
|
+
* print actionable guidance.
|
|
1829
|
+
*/
|
|
1830
|
+
function checkMacosFullDiskAccess(nodePath) {
|
|
1831
|
+
if (process.platform !== "darwin") {
|
|
1832
|
+
return { skipped: true };
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Resolve symlinks — TCC tracks the real binary path, not the symlink.
|
|
1836
|
+
// /opt/homebrew/bin/node is typically a symlink into the Cellar.
|
|
1837
|
+
let realNodePath = nodePath;
|
|
1838
|
+
try {
|
|
1839
|
+
realNodePath = realpathSync(nodePath);
|
|
1840
|
+
} catch {
|
|
1841
|
+
// Fall back to the symlink path if resolution fails.
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
let hasFDA = false;
|
|
1845
|
+
try {
|
|
1846
|
+
accessSync(resolve(homedir(), "Library", "Mail"), fsConstants.R_OK);
|
|
1847
|
+
hasFDA = true;
|
|
1848
|
+
} catch {
|
|
1849
|
+
// Either Mail isn't there (very rare) or FDA is missing. Default to
|
|
1850
|
+
// "missing" — false negatives just trigger a warning the user can ignore.
|
|
1851
|
+
hasFDA = false;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
return {
|
|
1855
|
+
skipped: false,
|
|
1856
|
+
realNodePath,
|
|
1857
|
+
hasFDA,
|
|
1858
|
+
paneUrl: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles",
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1560
1862
|
/**
|
|
1561
1863
|
* Render the launchd plist that runs `node dist/index.js` as a per-user
|
|
1562
1864
|
* agent. Inherits the GUI login session so the macOS Keychain is
|
|
@@ -1726,6 +2028,40 @@ async function launchdInstall() {
|
|
|
1726
2028
|
console.log("");
|
|
1727
2029
|
console.log("💡 pm2 still has other projects running — leaving it installed.");
|
|
1728
2030
|
}
|
|
2031
|
+
|
|
2032
|
+
// ── Full Disk Access reminder ────────────────────────────────────────────
|
|
2033
|
+
//
|
|
2034
|
+
// Under launchd, the bot has no foreground Terminal to inherit TCC from.
|
|
2035
|
+
// Anything it spawns (codex CLI, file-reading skills, etc.) needs `node`
|
|
2036
|
+
// itself to have Full Disk Access — otherwise reads silently fail.
|
|
2037
|
+
const fda = checkMacosFullDiskAccess(nodePath);
|
|
2038
|
+
if (!fda.skipped && !fda.hasFDA) {
|
|
2039
|
+
console.log("");
|
|
2040
|
+
console.log("⚠️ Full Disk Access not granted to node.");
|
|
2041
|
+
console.log("");
|
|
2042
|
+
console.log(" Under launchd, the bot can't ask for permission dialogs interactively.");
|
|
2043
|
+
console.log(" Without FDA, anything the bot spawns (codex CLI, file-reading skills,");
|
|
2044
|
+
console.log(" ~/Documents access, …) will silently fail to read protected files.");
|
|
2045
|
+
console.log("");
|
|
2046
|
+
console.log(" Grant once: System Settings → Privacy & Security → Full Disk Access → +");
|
|
2047
|
+
console.log(" Then add this exact path (⌘⇧G to paste it):");
|
|
2048
|
+
console.log("");
|
|
2049
|
+
console.log(` ${fda.realNodePath}`);
|
|
2050
|
+
console.log("");
|
|
2051
|
+
console.log(" Open the right pane now:");
|
|
2052
|
+
console.log(` open "${fda.paneUrl}"`);
|
|
2053
|
+
console.log("");
|
|
2054
|
+
console.log(" After granting, restart the bot to pick up the new permission:");
|
|
2055
|
+
console.log(` launchctl kickstart -k gui/$UID/${label}`);
|
|
2056
|
+
console.log("");
|
|
2057
|
+
console.log(" Note: re-grant is required after `brew upgrade node` — TCC binds to");
|
|
2058
|
+
console.log(" the Cellar version path, which changes when node is upgraded.");
|
|
2059
|
+
} else if (!fda.skipped && fda.hasFDA) {
|
|
2060
|
+
console.log("");
|
|
2061
|
+
console.log("✅ Full Disk Access already granted to node — spawned tools can read");
|
|
2062
|
+
console.log(` protected files. (Granted path: ${fda.realNodePath})`);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
1729
2065
|
process.exit(0);
|
|
1730
2066
|
}
|
|
1731
2067
|
|
|
@@ -1826,6 +2162,23 @@ switch (cmd) {
|
|
|
1826
2162
|
case "doctor":
|
|
1827
2163
|
doctor().catch(console.error);
|
|
1828
2164
|
break;
|
|
2165
|
+
case "tools": {
|
|
2166
|
+
const sub = process.argv[3] || "list";
|
|
2167
|
+
if (sub === "list" || sub === "ls" || sub === "status") {
|
|
2168
|
+
listOptionalTools();
|
|
2169
|
+
} else if (sub === "install") {
|
|
2170
|
+
runOptionalToolsStep().then(() => closeRL()).catch(err => {
|
|
2171
|
+
console.error(err);
|
|
2172
|
+
closeRL();
|
|
2173
|
+
});
|
|
2174
|
+
} else {
|
|
2175
|
+
console.log("Usage: alvin-bot tools <list|install>");
|
|
2176
|
+
console.log("");
|
|
2177
|
+
console.log(" list — Show installed / missing optional tools.");
|
|
2178
|
+
console.log(" install — Interactive menu to install missing tools.");
|
|
2179
|
+
}
|
|
2180
|
+
break;
|
|
2181
|
+
}
|
|
1829
2182
|
case "update":
|
|
1830
2183
|
update().catch(console.error);
|
|
1831
2184
|
break;
|
|
@@ -2188,6 +2541,7 @@ ${t("cli.commands")}
|
|
|
2188
2541
|
doctor ${t("cli.doctorDesc")}
|
|
2189
2542
|
audit Security health check (permissions, secrets, config)
|
|
2190
2543
|
search Search your assets, memories, and skills
|
|
2544
|
+
tools List / install optional capability tools (ffmpeg, pandoc, …)
|
|
2191
2545
|
browser Manage bot-owned Chromium (start/stop/goto/shot/doctor)
|
|
2192
2546
|
update ${t("cli.updateDesc")}
|
|
2193
2547
|
start ${t("cli.startDesc")} (background via PM2)
|
|
@@ -91,6 +91,12 @@ export class CodexCLIProvider {
|
|
|
91
91
|
timeout: 120_000,
|
|
92
92
|
env: { ...process.env, NO_COLOR: "1" },
|
|
93
93
|
});
|
|
94
|
+
// Close stdin so `codex exec` doesn't wait for additional input — the
|
|
95
|
+
// prompt is passed as a positional arg, no stdin payload follows.
|
|
96
|
+
// Without this, codex prints "Reading additional input from stdin..."
|
|
97
|
+
// and blocks until the 120s timeout, causing the bot to return
|
|
98
|
+
// "keine Antwort" / empty replies on the chat side.
|
|
99
|
+
proc.stdin.end();
|
|
94
100
|
let stdout = "";
|
|
95
101
|
let stderr = "";
|
|
96
102
|
proc.stdout.on("data", (data) => {
|
package/install.sh
CHANGED
|
@@ -54,22 +54,111 @@ check_git() {
|
|
|
54
54
|
ok "Git found: $(git --version)"
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
# Is this shell interactive enough to prompt the user?
|
|
58
|
+
is_interactive() {
|
|
59
|
+
[ -t 0 ] && [ -t 1 ]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Ensure Homebrew is available on macOS. Offers to auto-install via the
|
|
63
|
+
# official curl|bash sequence. A no-op on non-macOS systems.
|
|
64
|
+
ensure_brew() {
|
|
65
|
+
[ "$OS" = "macOS" ] || return 0
|
|
66
|
+
if command -v brew &>/dev/null; then
|
|
67
|
+
ok "Homebrew found: $(brew --version | head -1)"
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
warn "Homebrew is not installed."
|
|
71
|
+
if ! is_interactive; then
|
|
72
|
+
fail "Non-interactive shell — cannot prompt. Install Homebrew manually: https://brew.sh"
|
|
63
73
|
fi
|
|
74
|
+
echo ""
|
|
75
|
+
read -r -p " Install Homebrew automatically? [Y/n] " ans
|
|
76
|
+
case "${ans:-Y}" in
|
|
77
|
+
[Yy]*)
|
|
78
|
+
info "Installing Homebrew (the official installer may prompt for sudo)..."
|
|
79
|
+
# NONINTERACTIVE=1 tells brew's installer to skip the ENTER-to-confirm
|
|
80
|
+
# pause — the user already opted in here.
|
|
81
|
+
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \
|
|
82
|
+
|| fail "Homebrew install failed. See https://brew.sh for manual instructions."
|
|
83
|
+
# Add brew to PATH for the remainder of this script.
|
|
84
|
+
if [ -x /opt/homebrew/bin/brew ]; then
|
|
85
|
+
eval "$(/opt/homebrew/bin/brew shellenv)"
|
|
86
|
+
elif [ -x /usr/local/bin/brew ]; then
|
|
87
|
+
eval "$(/usr/local/bin/brew shellenv)"
|
|
88
|
+
fi
|
|
89
|
+
ok "Homebrew installed."
|
|
90
|
+
;;
|
|
91
|
+
*)
|
|
92
|
+
fail "Cannot continue without Homebrew. Install manually: https://brew.sh"
|
|
93
|
+
;;
|
|
94
|
+
esac
|
|
95
|
+
}
|
|
64
96
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
97
|
+
# Ensure Node.js >= MIN_NODE_VERSION. Offers to install via brew (macOS) or
|
|
98
|
+
# NodeSource (Debian/Ubuntu) if missing or too old. Falls back to a clear
|
|
99
|
+
# manual-install message on unsupported platforms.
|
|
100
|
+
ensure_node() {
|
|
101
|
+
if command -v node &>/dev/null; then
|
|
102
|
+
local node_ver
|
|
103
|
+
node_ver=$(node -v | sed 's/^v//' | cut -d. -f1)
|
|
104
|
+
if [ "$node_ver" -ge "$MIN_NODE_VERSION" ]; then
|
|
105
|
+
ok "Node.js found: $(node -v)"
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
warn "Node.js v$node_ver is too old (need >= $MIN_NODE_VERSION)."
|
|
109
|
+
else
|
|
110
|
+
warn "Node.js is not installed."
|
|
69
111
|
fi
|
|
70
|
-
|
|
112
|
+
|
|
113
|
+
if ! is_interactive; then
|
|
114
|
+
fail "Non-interactive shell — install Node >= $MIN_NODE_VERSION manually: https://nodejs.org"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
case "$OS" in
|
|
118
|
+
macOS)
|
|
119
|
+
ensure_brew
|
|
120
|
+
echo ""
|
|
121
|
+
read -r -p " Install Node via Homebrew? [Y/n] " ans
|
|
122
|
+
case "${ans:-Y}" in
|
|
123
|
+
[Yy]*)
|
|
124
|
+
info "Installing Node (latest LTS via brew)..."
|
|
125
|
+
brew install node || fail "brew install node failed."
|
|
126
|
+
ok "Node installed: $(node -v)"
|
|
127
|
+
;;
|
|
128
|
+
*)
|
|
129
|
+
fail "Cannot continue without Node. Install manually: https://nodejs.org"
|
|
130
|
+
;;
|
|
131
|
+
esac
|
|
132
|
+
;;
|
|
133
|
+
Linux|WSL)
|
|
134
|
+
if command -v apt-get &>/dev/null; then
|
|
135
|
+
echo ""
|
|
136
|
+
warn "About to install Node 22 from the official NodeSource repo. This needs sudo."
|
|
137
|
+
read -r -p " Continue? [Y/n] " ans
|
|
138
|
+
case "${ans:-Y}" in
|
|
139
|
+
[Yy]*)
|
|
140
|
+
info "Adding NodeSource repo..."
|
|
141
|
+
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - \
|
|
142
|
+
|| fail "NodeSource setup failed."
|
|
143
|
+
info "Installing nodejs..."
|
|
144
|
+
sudo apt-get install -y nodejs || fail "apt-get install nodejs failed."
|
|
145
|
+
ok "Node installed: $(node -v)"
|
|
146
|
+
;;
|
|
147
|
+
*)
|
|
148
|
+
fail "Cannot continue without Node. Install manually: https://nodejs.org"
|
|
149
|
+
;;
|
|
150
|
+
esac
|
|
151
|
+
else
|
|
152
|
+
fail "Auto-install on Linux only supports apt-based distros. Install Node manually: https://nodejs.org"
|
|
153
|
+
fi
|
|
154
|
+
;;
|
|
155
|
+
*)
|
|
156
|
+
fail "Cannot bootstrap Node on $OS — install manually."
|
|
157
|
+
;;
|
|
158
|
+
esac
|
|
71
159
|
}
|
|
72
160
|
|
|
161
|
+
# npm comes bundled with node, but verify just in case.
|
|
73
162
|
check_npm() {
|
|
74
163
|
if ! command -v npm &>/dev/null; then
|
|
75
164
|
fail "npm is not installed. It should come with Node.js — please reinstall Node."
|
|
@@ -136,7 +225,7 @@ main() {
|
|
|
136
225
|
|
|
137
226
|
detect_os
|
|
138
227
|
check_git
|
|
139
|
-
|
|
228
|
+
ensure_node
|
|
140
229
|
check_npm
|
|
141
230
|
|
|
142
231
|
echo ""
|
package/package.json
CHANGED
package/02_yandex.png
DELETED
|
Binary file
|