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 +32 -0
- package/README.md +57 -0
- package/bin/becki.js +31 -0
- package/dist/cli.js +77 -0
- package/dist/client.js +286 -0
- package/dist/commands.js +321 -0
- package/dist/config.js +138 -0
- package/dist/entry.js +14 -0
- package/dist/handoff.js +54 -0
- package/dist/loops.js +127 -0
- package/dist/menu.js +108 -0
- package/dist/prompt.js +121 -0
- package/dist/screens/_shared.js +67 -0
- package/dist/screens/account.js +35 -0
- package/dist/screens/backfills.js +92 -0
- package/dist/screens/diagnostics.js +87 -0
- package/dist/screens/git.js +211 -0
- package/dist/screens/install-token.js +65 -0
- package/dist/screens/logs.js +74 -0
- package/dist/screens/status.js +77 -0
- package/dist/screens/subscription.js +60 -0
- package/dist/screens/vault.js +155 -0
- package/dist/screens/watch-folders.js +406 -0
- package/dist/setup.js +71 -0
- package/dist/splash.js +27 -0
- package/dist/theme.js +38 -0
- package/dist/vault.js +357 -0
- package/package.json +59 -0
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
|
+
}
|