becki 0.5.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/LICENSE ADDED
@@ -0,0 +1,32 @@
1
+ becki — Becki Core admin TUI
2
+ Copyright © 2026 BECKI.IO LLC
3
+
4
+ All rights reserved.
5
+
6
+ This npm package and its compiled JavaScript are distributed as a runtime
7
+ client for the Becki service (https://www.becki.io). Use of this software
8
+ requires a valid paid subscription and is governed by the Becki Terms of
9
+ Service at https://www.becki.io/terms and the End User License Agreement
10
+ at https://www.becki.io/eula.
11
+
12
+ You are permitted to:
13
+ - Install this package and run the `becki` binary on machines you own
14
+ or control, for personal or business use, in conjunction with a valid
15
+ Becki subscription tied to your account.
16
+ - Inspect the compiled JavaScript for security or compatibility purposes.
17
+
18
+ You are NOT permitted to:
19
+ - Redistribute this package, its source, or modified versions.
20
+ - Reverse-engineer, decompile, or extract the binary for purposes of
21
+ creating a competing product or service.
22
+ - Use this software with any backend other than the official Becki
23
+ backend operated by BECKI.IO LLC, except where explicitly authorized
24
+ in writing.
25
+ - Remove or modify the copyright notices, the LICENSE file, or any
26
+ notices embedded in the source.
27
+
28
+ This software is provided "as-is" without warranty of any kind, express or
29
+ implied. BECKI.IO LLC will not be liable for any damages arising from the
30
+ use or inability to use this software.
31
+
32
+ For licensing inquiries, contact: legal@becki.io
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # becki
2
+
3
+ A terminal command that runs before your AI CLI. It reads your **local Becki
4
+ vault** from the filesystem, shows a splash (vault entry count, last-ingested
5
+ entry, your open threads), optionally asks "what are you working on today?",
6
+ then spawns your configured AI CLI — passing the typed focus as the CLI's
7
+ first prompt arg.
8
+
9
+ It never injects context into the CLI and never makes a network call. The CLI
10
+ already has Becki via its own MCP connection; `becki` just makes the invisible
11
+ memory layer visible for ten seconds and launches you in oriented.
12
+
13
+ ## Install (local, dogfood)
14
+
15
+ ```sh
16
+ cd ~/Repos/becki-shell
17
+ npm install
18
+ npm run build
19
+ npm link # makes `becki` available on your PATH
20
+ ```
21
+
22
+ Then just run:
23
+
24
+ ```sh
25
+ becki # splash → prompt → launch claude
26
+ becki --resume # passthrough args flow to the CLI after the focus
27
+ ```
28
+
29
+ ## Config
30
+
31
+ `~/.becki/config.json` is auto-created with defaults on first run.
32
+
33
+ | Key | Default | Description |
34
+ |-----|---------|-------------|
35
+ | `cli` | `claude` | CLI command to hand off to |
36
+ | `vault_path` | `~/Documents/Becki` | Local Becki vault directory to read |
37
+ | `show_open_threads` | `true` | Show the open-threads list on the splash |
38
+ | `max_open_threads` | `5` | How many open threads to list |
39
+ | `skip_prompt` | `false` | Skip the "what are you working on?" prompt |
40
+ | `pass_focus_to_cli` | `true` | Pass a typed focus as the CLI's first prompt arg |
41
+
42
+ ## Graceful degradation
43
+
44
+ `becki` is a launcher first. A missing/unreadable vault, a malformed config,
45
+ or a non-TTY environment all degrade to a one-line warning — `becki` still
46
+ launches the CLI. The one loud failure is a `cli` binary that cannot be
47
+ found: it exits 127 with a clear message rather than launching nothing.
48
+
49
+ ## Modules
50
+
51
+ - `src/config.ts` — load / create / validate `~/.becki/config.json`
52
+ - `src/vault.ts` — local vault read: entry count, last ingest, open threads
53
+ - `src/splash.tsx` — Ink UI: mark, vault stats, open-threads list
54
+ - `src/prompt.tsx` — Ink UI: the focus prompt + the App that ties it together
55
+ - `src/handoff.ts` — spawn the CLI, inherit the TTY, propagate the exit code
56
+ - `src/cli.ts` — orchestrator (config → vault → splash → handoff)
57
+ - `bin/becki.js` — package `bin` entry (shebang; delegates to `dist/cli.js`)
package/bin/becki.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * becki — terminal dashboard + launcher for your Becki operational brain.
4
+ *
5
+ * This is the package `bin` entry. It is intentionally tiny: it delegates to
6
+ * the compiled orchestrator in dist/. Run `npm run build` first so dist/
7
+ * exists. Any unexpected crash is caught here so becki fails with a clear
8
+ * message rather than a raw stack trace.
9
+ */
10
+
11
+ import { fileURLToPath } from 'node:url';
12
+ import { dirname, join } from 'node:path';
13
+ import { existsSync } from 'node:fs';
14
+
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+ const entry = join(here, '..', 'dist', 'cli.js');
17
+
18
+ if (!existsSync(entry)) {
19
+ process.stderr.write(
20
+ 'becki: build output missing — run "npm run build" in the becki-shell repo.\n',
21
+ );
22
+ process.exit(1);
23
+ }
24
+
25
+ try {
26
+ const { main } = await import(entry);
27
+ await main();
28
+ } catch (err) {
29
+ process.stderr.write(`becki: unexpected error — ${err?.message ?? err}\n`);
30
+ process.exit(1);
31
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * cli.ts — entry point. Compiled to dist/cli.js, invoked by bin/becki.js.
3
+ *
4
+ * Modes:
5
+ * `becki` → open admin TUI (the menu router)
6
+ * `becki ask|loops|save|resolve …` → operate on the vault and exit
7
+ * `becki --help` → usage
8
+ * `becki --version` → npm package version
9
+ *
10
+ * The launcher mode (splash → prompt → handoff to claude) was stripped on
11
+ * 2026-05-28 — Bryan found it redundant ("I prefer to cut right into resuming
12
+ * my session"). See [[becki-shell-repurposed-as-admin-tui]].
13
+ */
14
+ import React from 'react';
15
+ import { render } from 'ink';
16
+ import { isCommand, runCommand } from './commands.js';
17
+ import { MenuApp } from './menu.js';
18
+ const HELP_TEXT = `becki — Becki Core admin TUI + vault CLI
19
+
20
+ Usage:
21
+ becki Open admin TUI (Status / Watch folders / Backfills / ...)
22
+ becki ask <query> Semantic search the vault
23
+ becki loops List open threads
24
+ becki save --type <t> ... Save an entry to the vault
25
+ becki resolve <row-id> Mark a vault row resolved
26
+ becki --help, -h Show this help
27
+ becki --version, -v Show version
28
+
29
+ Companion daemon:
30
+ becki-mcp The MCP server AI clients connect to
31
+ becki-mcp init Set up cache + register projects (programmatic)
32
+ becki-mcp digest Run today's session digest
33
+ becki-mcp bootstrap [N] Historical ingest (default 90 days)
34
+
35
+ Docs: https://www.becki.io
36
+ `;
37
+ export async function main() {
38
+ const rawArgs = process.argv.slice(2);
39
+ if (rawArgs[0] === '--help' || rawArgs[0] === '-h') {
40
+ process.stdout.write(HELP_TEXT);
41
+ process.exit(0);
42
+ }
43
+ if (rawArgs[0] === '--version' || rawArgs[0] === '-v') {
44
+ // Lazy import so plain --version doesn't trigger a config-load side effect.
45
+ try {
46
+ const { readFileSync } = await import('node:fs');
47
+ const { fileURLToPath } = await import('node:url');
48
+ const { dirname, join } = await import('node:path');
49
+ const here = dirname(fileURLToPath(import.meta.url));
50
+ const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
51
+ process.stdout.write(`becki ${pkg.version}\n`);
52
+ }
53
+ catch {
54
+ process.stdout.write('becki (version unknown)\n');
55
+ }
56
+ process.exit(0);
57
+ }
58
+ // Subcommand mode — bypass the TUI entirely.
59
+ if (isCommand(rawArgs[0])) {
60
+ process.exit(await runCommand(rawArgs[0], rawArgs.slice(1)));
61
+ }
62
+ // No subcommand → admin TUI.
63
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
64
+ process.stderr.write('becki: admin TUI needs an interactive terminal\n');
65
+ process.stderr.write(' use `becki --help` for non-interactive subcommands\n');
66
+ process.exit(1);
67
+ }
68
+ const { waitUntilExit } = render(React.createElement(MenuApp), {
69
+ exitOnCtrlC: true,
70
+ });
71
+ await waitUntilExit();
72
+ // Quick goodbye after the TUI tears down so the exit doesn't feel
73
+ // like a crash. stdout (not stderr) so it stays visible on success
74
+ // and goes to the terminal even with shells that redirect stderr.
75
+ process.stdout.write('\n ◆ becki — see you soon.\n\n');
76
+ process.exit(0);
77
+ }
package/dist/client.js ADDED
@@ -0,0 +1,286 @@
1
+ /**
2
+ * client.ts — becki vault API client (chunk 1a: install-token auth).
3
+ *
4
+ * Talks to the hosted Becki vault (Supabase) over HTTP, authenticating with
5
+ * the install token + user-id that Becki.app writes to
6
+ * ~/Library/Application Support/Becki/. This is the same credential path the
7
+ * Mac-app-bundled becki-mcp stdio server uses — so `becki ask/loops/save/
8
+ * resolve` work on any Mac with Becki.app installed and signed in once.
9
+ *
10
+ * Cross-platform note: a genuine non-Mac machine has no Becki.app and no
11
+ * install token — that's chunk 1b, an OAuth device flow against the hosted
12
+ * MCP. Everything here takes { userId, ingestToken } via resolveAuth(), so a
13
+ * future OAuth resolver can supply the same shape with no other change.
14
+ */
15
+ import { homedir, platform } from 'node:os';
16
+ import { join } from 'node:path';
17
+ import { existsSync, readFileSync, writeFileSync, statSync, mkdirSync, } from 'node:fs';
18
+ import { randomUUID } from 'node:crypto';
19
+ export const SUPABASE_URL = 'https://mbutedmgtkmoigfbrthr.supabase.co';
20
+ // Publishable anon key — the same one bundled in becki-mcp. Every read RPC is
21
+ // scoped by an explicit pp_user_id, so the anon key alone grants nothing.
22
+ export const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1idXRlZG1ndGttb2lnZmJydGhyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY4Mjk1NTYsImV4cCI6MjA5MjQwNTU1Nn0.FHMSjKrwSgglGa6__ykxuPjxCrjmj8gKqwpEbJoVvgk';
23
+ /**
24
+ * Cross-platform Becki home directory.
25
+ *
26
+ * Resolution order matches becki-mcp's resolveBeckiHome():
27
+ * 1. BECKI_HOME env var override
28
+ * 2. On macOS with the legacy Application Support dir present (Becki.app
29
+ * is installed), keep using it — shared state with the Mac app
30
+ * 3. Otherwise ~/.becki/ (Core default everywhere else)
31
+ */
32
+ export function resolveBeckiHome() {
33
+ const envOverride = process.env.BECKI_HOME;
34
+ if (envOverride && envOverride.length > 0)
35
+ return envOverride;
36
+ if (platform() === 'darwin') {
37
+ const legacy = join(homedir(), 'Library', 'Application Support', 'Becki');
38
+ if (existsSync(legacy))
39
+ return legacy;
40
+ }
41
+ return join(homedir(), '.becki');
42
+ }
43
+ const APP_SUPPORT_DIR = resolveBeckiHome();
44
+ const USER_ID_PATH = join(APP_SUPPORT_DIR, 'mcp-user-id');
45
+ const INGEST_TOKEN_PATH = join(APP_SUPPORT_DIR, 'mcp-ingest-token');
46
+ const INSTALL_ID_PATH = join(homedir(), '.becki', 'install-id');
47
+ export const PATHS = {
48
+ beckiHome: APP_SUPPORT_DIR,
49
+ userId: USER_ID_PATH,
50
+ ingestToken: INGEST_TOKEN_PATH,
51
+ installId: INSTALL_ID_PATH,
52
+ };
53
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
54
+ /** match_vault_v5 returns RRF-fused scores; this ceiling normalizes to 0..1. */
55
+ const RRF_CEILING = 0.0328;
56
+ /** Vault entry types accepted by `becki save --type`. */
57
+ export const SOURCE_TYPES = [
58
+ 'decision',
59
+ 'commitment',
60
+ 'note',
61
+ 'open_loop',
62
+ 'dead_end',
63
+ ];
64
+ // --- Auth --------------------------------------------------------------
65
+ /**
66
+ * Read a 0600-protected credential file Becki.app wrote. Any other mode means
67
+ * the file was touched by something else — refuse it. Mirrors becki-mcp.
68
+ */
69
+ function readProtected(path) {
70
+ try {
71
+ if (!existsSync(path))
72
+ return null;
73
+ if ((statSync(path).mode & 0o777) !== 0o600)
74
+ return null;
75
+ return readFileSync(path, 'utf8').trim() || null;
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ /** Resolve vault credentials from the local Becki.app install. */
82
+ export function resolveAuth() {
83
+ const rawUser = readProtected(USER_ID_PATH);
84
+ return {
85
+ userId: rawUser && UUID_RE.test(rawUser) ? rawUser : null,
86
+ ingestToken: readProtected(INGEST_TOKEN_PATH),
87
+ };
88
+ }
89
+ /** A stable per-machine id for anon embed-quota attribution. Created once. */
90
+ function installId() {
91
+ try {
92
+ if (existsSync(INSTALL_ID_PATH)) {
93
+ const v = readFileSync(INSTALL_ID_PATH, 'utf8').trim();
94
+ if (v)
95
+ return v;
96
+ }
97
+ }
98
+ catch {
99
+ // fall through to mint a fresh one
100
+ }
101
+ const id = randomUUID();
102
+ try {
103
+ mkdirSync(join(homedir(), '.becki'), { recursive: true });
104
+ writeFileSync(INSTALL_ID_PATH, id + '\n', 'utf8');
105
+ }
106
+ catch {
107
+ // non-fatal — a fresh id each run still works, just no quota continuity
108
+ }
109
+ return id;
110
+ }
111
+ // --- HTTP helpers ------------------------------------------------------
112
+ /** Headers for a PostgREST RPC call on the `becki` schema. */
113
+ function rpcHeaders() {
114
+ return {
115
+ 'Content-Type': 'application/json',
116
+ Accept: 'application/json',
117
+ apikey: SUPABASE_ANON_KEY,
118
+ Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
119
+ 'Content-Profile': 'becki',
120
+ };
121
+ }
122
+ async function failBody(res, label) {
123
+ const body = await res.text().catch(() => '');
124
+ throw new Error(`${label} ${res.status}: ${body.slice(0, 200)}`);
125
+ }
126
+ /** Embed query text via the voyage-embed edge function (anon query path). */
127
+ async function embedQuery(text) {
128
+ const res = await fetch(`${SUPABASE_URL}/functions/v1/voyage-embed`, {
129
+ method: 'POST',
130
+ headers: {
131
+ 'Content-Type': 'application/json',
132
+ apikey: SUPABASE_ANON_KEY,
133
+ Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
134
+ 'X-Becki-Install-Id': installId(),
135
+ },
136
+ body: JSON.stringify({ texts: [text], input_type: 'query' }),
137
+ });
138
+ if (res.status === 429) {
139
+ throw new Error('NeuraVault is rate-limited — daily query quota exhausted. Try again later.');
140
+ }
141
+ if (!res.ok)
142
+ await failBody(res, 'voyage-embed');
143
+ const json = (await res.json());
144
+ if (!Array.isArray(json.vectors?.[0])) {
145
+ throw new Error('voyage-embed returned no vector');
146
+ }
147
+ return json.vectors[0];
148
+ }
149
+ // --- Vault operations --------------------------------------------------
150
+ /** Semantic vault search. Embeds the query, then calls match_vault_v5. */
151
+ export async function query(userId, text, limit) {
152
+ const embedding = await embedQuery(text);
153
+ const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/match_vault_v5`, {
154
+ method: 'POST',
155
+ headers: rpcHeaders(),
156
+ body: JSON.stringify({
157
+ pp_query_embedding: embedding,
158
+ pp_query_text: text,
159
+ pp_match_threshold: 0.2,
160
+ pp_match_count: limit,
161
+ pp_filter_source_types: null,
162
+ pp_filter_project_id: null,
163
+ pp_knowledge_boost: 0,
164
+ pp_candidate_pool: 60,
165
+ pp_include_resolved: false,
166
+ pp_recency_half_life_days: 90,
167
+ pp_since: null,
168
+ pp_until: null,
169
+ pp_recency_floor: 0.1,
170
+ pp_user_id: userId,
171
+ pp_resolution_filter: null,
172
+ }),
173
+ });
174
+ if (!res.ok)
175
+ await failBody(res, 'match_vault_v5');
176
+ const raw = (await res.json());
177
+ return raw.map((c) => ({
178
+ ...c,
179
+ similarity: Math.min(1, c.similarity / RRF_CEILING),
180
+ }));
181
+ }
182
+ /** List open threads — chronological dump of unresolved open_loop rows. */
183
+ export async function listLoops(userId, limit) {
184
+ const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/list_vault_rows`, {
185
+ method: 'POST',
186
+ headers: rpcHeaders(),
187
+ body: JSON.stringify({
188
+ p_project_id: null,
189
+ p_since: null,
190
+ p_until: null,
191
+ p_types: ['open_loop'],
192
+ p_limit: limit,
193
+ p_user_id: userId,
194
+ }),
195
+ });
196
+ if (!res.ok)
197
+ await failBody(res, 'list_vault_rows');
198
+ return (await res.json());
199
+ }
200
+ /** Ingest a vault entry via the ingest-vault-row edge function. */
201
+ export async function save(ingestToken, opts) {
202
+ const sourceId = opts.title ? `${todayStr()}-${toSlug(opts.title)}` : undefined;
203
+ const metadata = {};
204
+ const project = normProject(opts.project);
205
+ if (project)
206
+ metadata.project_name = project;
207
+ const res = await fetch(`${SUPABASE_URL}/functions/v1/ingest-vault-row`, {
208
+ method: 'POST',
209
+ headers: {
210
+ 'Content-Type': 'application/json',
211
+ Authorization: `Bearer ${ingestToken}`,
212
+ 'User-Agent': 'becki-shell',
213
+ },
214
+ body: JSON.stringify({
215
+ content: opts.content,
216
+ source_type: opts.type,
217
+ source_id: sourceId,
218
+ metadata,
219
+ }),
220
+ });
221
+ if (!res.ok)
222
+ await failBody(res, 'ingest-vault-row');
223
+ return (await res.json());
224
+ }
225
+ /** Mark a vault row resolved (close an open loop). */
226
+ export async function resolve(userId, rowId, note) {
227
+ const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/resolve_vault_row`, {
228
+ method: 'POST',
229
+ headers: rpcHeaders(),
230
+ body: JSON.stringify({
231
+ pp_row_id: rowId,
232
+ pp_resolved_by: 'cli',
233
+ pp_note: note ?? null,
234
+ pp_user_id: userId,
235
+ }),
236
+ });
237
+ if (!res.ok)
238
+ await failBody(res, 'resolve_vault_row');
239
+ const rows = (await res.json());
240
+ if (!Array.isArray(rows) || rows.length === 0) {
241
+ throw new Error(`row ${rowId} not found, not owned by you, or already resolved`);
242
+ }
243
+ }
244
+ /** Vault projects, via the get_viz_substrate RPC's project rollup. */
245
+ export async function listProjects(userId) {
246
+ const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/get_viz_substrate`, {
247
+ method: 'POST',
248
+ headers: rpcHeaders(),
249
+ // Tiny sample — the project rollup is what we want, not atomic items.
250
+ body: JSON.stringify({ p_user_id: userId, p_atomic_sample_size: 1 }),
251
+ });
252
+ if (!res.ok)
253
+ await failBody(res, 'get_viz_substrate');
254
+ const data = (await res.json());
255
+ return Array.isArray(data.projects) ? data.projects : [];
256
+ }
257
+ /** The most recent daily briefing, or null when none has been computed. */
258
+ export async function getBriefing(userId) {
259
+ const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/get_daily_briefing`, {
260
+ method: 'POST',
261
+ headers: rpcHeaders(),
262
+ body: JSON.stringify({ pp_user_id: userId }),
263
+ });
264
+ if (!res.ok)
265
+ await failBody(res, 'get_daily_briefing');
266
+ const rows = (await res.json());
267
+ return rows.length > 0 ? rows[0] : null;
268
+ }
269
+ // --- Small helpers (mirror becki-mcp) ----------------------------------
270
+ function todayStr() {
271
+ return new Date().toISOString().slice(0, 10);
272
+ }
273
+ function toSlug(s) {
274
+ return s
275
+ .toLowerCase()
276
+ .replace(/[^a-z0-9]+/g, '-')
277
+ .replace(/^-|-$/g, '')
278
+ .slice(0, 60);
279
+ }
280
+ /** Canonical project-slug form on write — mirrors becki.norm_project_slug. */
281
+ function normProject(raw) {
282
+ if (!raw)
283
+ return undefined;
284
+ const n = raw.trim().toLowerCase().replace(/_/g, '-');
285
+ return n || undefined;
286
+ }