tenbrains 2.1.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/AGENTS.md +112 -0
- package/README.md +218 -0
- package/dist/ai/analyzer.d.ts +28 -0
- package/dist/ai/analyzer.d.ts.map +1 -0
- package/dist/ai/analyzer.js +35 -0
- package/dist/ai/analyzer.js.map +1 -0
- package/dist/ai/client.d.ts +12 -0
- package/dist/ai/client.d.ts.map +1 -0
- package/dist/ai/client.js +152 -0
- package/dist/ai/client.js.map +1 -0
- package/dist/ai/json.d.ts +7 -0
- package/dist/ai/json.d.ts.map +1 -0
- package/dist/ai/json.js +57 -0
- package/dist/ai/json.js.map +1 -0
- package/dist/ai/mock.d.ts +6 -0
- package/dist/ai/mock.d.ts.map +1 -0
- package/dist/ai/mock.js +66 -0
- package/dist/ai/mock.js.map +1 -0
- package/dist/ai/prompts.d.ts +19 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +40 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/providers.d.ts +22 -0
- package/dist/ai/providers.d.ts.map +1 -0
- package/dist/ai/providers.js +56 -0
- package/dist/ai/providers.js.map +1 -0
- package/dist/ai/resolve.d.ts +24 -0
- package/dist/ai/resolve.d.ts.map +1 -0
- package/dist/ai/resolve.js +33 -0
- package/dist/ai/resolve.js.map +1 -0
- package/dist/bin/preflight.d.ts +2 -0
- package/dist/bin/preflight.d.ts.map +1 -0
- package/dist/bin/preflight.js +11 -0
- package/dist/bin/preflight.js.map +1 -0
- package/dist/bin/tenbrains.d.ts +3 -0
- package/dist/bin/tenbrains.d.ts.map +1 -0
- package/dist/bin/tenbrains.js +9 -0
- package/dist/bin/tenbrains.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +554 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/analyze.d.ts +7 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +204 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/bookmark.d.ts +9 -0
- package/dist/commands/bookmark.d.ts.map +1 -0
- package/dist/commands/bookmark.js +120 -0
- package/dist/commands/bookmark.js.map +1 -0
- package/dist/commands/config.d.ts +15 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +142 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/db.d.ts +9 -0
- package/dist/commands/db.d.ts.map +1 -0
- package/dist/commands/db.js +64 -0
- package/dist/commands/db.js.map +1 -0
- package/dist/commands/digest.d.ts +19 -0
- package/dist/commands/digest.d.ts.map +1 -0
- package/dist/commands/digest.js +71 -0
- package/dist/commands/digest.js.map +1 -0
- package/dist/commands/import.d.ts +11 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +78 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/learn.d.ts +9 -0
- package/dist/commands/learn.d.ts.map +1 -0
- package/dist/commands/learn.js +149 -0
- package/dist/commands/learn.js.map +1 -0
- package/dist/commands/manifest.d.ts +13 -0
- package/dist/commands/manifest.d.ts.map +1 -0
- package/dist/commands/manifest.js +62 -0
- package/dist/commands/manifest.js.map +1 -0
- package/dist/commands/record.d.ts +10 -0
- package/dist/commands/record.d.ts.map +1 -0
- package/dist/commands/record.js +37 -0
- package/dist/commands/record.js.map +1 -0
- package/dist/commands/search.d.ts +5 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +33 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/shared.d.ts +9 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +13 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/suggest.d.ts +9 -0
- package/dist/commands/suggest.d.ts.map +1 -0
- package/dist/commands/suggest.js +102 -0
- package/dist/commands/suggest.js.map +1 -0
- package/dist/commands/takeaway.d.ts +9 -0
- package/dist/commands/takeaway.d.ts.map +1 -0
- package/dist/commands/takeaway.js +167 -0
- package/dist/commands/takeaway.js.map +1 -0
- package/dist/core/config.d.ts +52 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +180 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/context.d.ts +28 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +40 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/errors.d.ts +33 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +82 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/input.d.ts +9 -0
- package/dist/core/input.d.ts.map +1 -0
- package/dist/core/input.js +47 -0
- package/dist/core/input.js.map +1 -0
- package/dist/core/opts.d.ts +8 -0
- package/dist/core/opts.d.ts.map +1 -0
- package/dist/core/opts.js +38 -0
- package/dist/core/opts.js.map +1 -0
- package/dist/core/output.d.ts +58 -0
- package/dist/core/output.d.ts.map +1 -0
- package/dist/core/output.js +76 -0
- package/dist/core/output.js.map +1 -0
- package/dist/core/paths.d.ts +20 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +31 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/text.d.ts +10 -0
- package/dist/core/text.d.ts.map +1 -0
- package/dist/core/text.js +132 -0
- package/dist/core/text.js.map +1 -0
- package/dist/core/validate.d.ts +5 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +17 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/db/database.d.ts +27 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.js +90 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/fts.d.ts +12 -0
- package/dist/db/fts.d.ts.map +1 -0
- package/dist/db/fts.js +42 -0
- package/dist/db/fts.js.map +1 -0
- package/dist/db/migrations.d.ts +20 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +243 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/repositories.d.ts +130 -0
- package/dist/db/repositories.d.ts.map +1 -0
- package/dist/db/repositories.js +418 -0
- package/dist/db/repositories.js.map +1 -0
- package/dist/domain/learn.d.ts +25 -0
- package/dist/domain/learn.d.ts.map +1 -0
- package/dist/domain/learn.js +85 -0
- package/dist/domain/learn.js.map +1 -0
- package/dist/domain/schemas.d.ts +169 -0
- package/dist/domain/schemas.d.ts.map +1 -0
- package/dist/domain/schemas.js +54 -0
- package/dist/domain/schemas.js.map +1 -0
- package/dist/domain/search.d.ts +32 -0
- package/dist/domain/search.d.ts.map +1 -0
- package/dist/domain/search.js +77 -0
- package/dist/domain/search.js.map +1 -0
- package/dist/domain/suggest.d.ts +17 -0
- package/dist/domain/suggest.d.ts.map +1 -0
- package/dist/domain/suggest.js +114 -0
- package/dist/domain/suggest.js.map +1 -0
- package/dist/domain/tags.d.ts +9 -0
- package/dist/domain/tags.d.ts.map +1 -0
- package/dist/domain/tags.js +40 -0
- package/dist/domain/tags.js.map +1 -0
- package/dist/domain/types.d.ts +84 -0
- package/dist/domain/types.d.ts.map +1 -0
- package/dist/domain/types.js +2 -0
- package/dist/domain/types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/x/archive.d.ts +32 -0
- package/dist/x/archive.d.ts.map +1 -0
- package/dist/x/archive.js +108 -0
- package/dist/x/archive.js.map +1 -0
- package/dist/x/client.d.ts +61 -0
- package/dist/x/client.d.ts.map +1 -0
- package/dist/x/client.js +321 -0
- package/dist/x/client.js.map +1 -0
- package/package.json +58 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Driving tenbrains from an agent
|
|
2
|
+
|
|
3
|
+
This CLI is built to be called by automated agents. Read this once; call `tenbrains manifest` for the
|
|
4
|
+
exhaustive, always-current spec.
|
|
5
|
+
|
|
6
|
+
## Contract
|
|
7
|
+
|
|
8
|
+
- **stdout is exactly one JSON object per run.** Parse it. Never scrape human text.
|
|
9
|
+
- **stderr is diagnostics only** (progress, warnings). Ignore it for results, or surface it to logs.
|
|
10
|
+
- **Branch on `ok`, then on `error.code`** — not on message strings.
|
|
11
|
+
- **Exit code mirrors the error class** (see table). `0` iff `ok: true`.
|
|
12
|
+
|
|
13
|
+
Success envelope:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{ "ok": true, "command": "<name>", "data": { ... }, "meta": { ... } }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Error envelope:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{ "ok": false, "command": "<name>", "error": { "code": "<CODE>", "message": "...", "retryable": false, "details": { ... } } }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`meta` carries ids and flags you'll want to chain on: `analysisId`, `postId`, `snapshotId`,
|
|
26
|
+
`bookmarkId`, `suggestionId`, `trackId`, `provider`, `model`, `mock`, `deduped`, `persisted`.
|
|
27
|
+
|
|
28
|
+
## Codes
|
|
29
|
+
|
|
30
|
+
| error.code | exit | meaning |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| (none — success) | 0 | `ok: true` |
|
|
33
|
+
| INTERNAL | 1 | unexpected failure |
|
|
34
|
+
| USAGE | 2 | bad/missing arguments or unknown command |
|
|
35
|
+
| NOT_FOUND | 3 | id or resource does not exist |
|
|
36
|
+
| MISSING_CREDENTIALS / CONFIG_ERROR | 4 | no key configured / unreadable config |
|
|
37
|
+
| PROVIDER_* | 5 | upstream model error (`retryable` indicates if a retry may help) |
|
|
38
|
+
| VALIDATION / PROVIDER_BAD_OUTPUT | 6 | input or model output failed schema validation |
|
|
39
|
+
| CONFLICT | 7 | already exists (e.g. duplicate bookmark / follow) |
|
|
40
|
+
|
|
41
|
+
## Conventions
|
|
42
|
+
|
|
43
|
+
- **Output is JSON by default.** Do not pass `--pretty` (that's for humans).
|
|
44
|
+
- **Pass content in, or fetch it.** Provide post text via `--text`, or give `--url`/`--id` to fetch a
|
|
45
|
+
tweet — single tweets fetch free via oEmbed (no key), `--fetch auto|oembed|api`. Inputs accept
|
|
46
|
+
inline strings, `@path` (read file), or `-` (read stdin). JSON inputs (`--posts`, `--ratings`) take
|
|
47
|
+
the same forms. `takeaway refresh` fetches a timeline when `--posts` is omitted (needs an X token,
|
|
48
|
+
usually a paid tier).
|
|
49
|
+
- **Ids are prefixed and stable**: `post_`, `ana_`, `acc_`, `snap_`, `bm_`, `sug_`, `trk_`. Any id
|
|
50
|
+
resolves via `tenbrains record get <id>`.
|
|
51
|
+
- **Dedup is automatic.** Re-ingesting a post with the same `--id` (external X id) reuses the stored
|
|
52
|
+
post (`meta.deduped: true`).
|
|
53
|
+
- **Isolate state with `--db <path>`** if you want a per-task workspace.
|
|
54
|
+
- **No environment variables.** Configure once with `tenbrains setup --provider <p> --api-key <k>`
|
|
55
|
+
(add `--x-bearer <token>` for X timeline fetches; `-` pipes a secret without echo). Or test offline
|
|
56
|
+
with `--provider mock`.
|
|
57
|
+
|
|
58
|
+
## Recipes
|
|
59
|
+
|
|
60
|
+
Analyze and capture the id (supply text, or fetch a tweet free by URL):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
tenbrains analyze --provider mock --id 123 --author levelsio \
|
|
64
|
+
--text "Agent-first CLIs persist outcomes to a database."
|
|
65
|
+
tenbrains analyze --url "https://x.com/jack/status/20" # fetched free via oEmbed; meta.source=x:oembed
|
|
66
|
+
# -> .meta.analysisId, .data.analysis.{topic,summary,intent,novelConcepts[5]}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Analyze + generate a learning track in one call (ratings optional):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
echo '[{"concept":"Agentic","familiarity":2,"interest":5}]' \
|
|
73
|
+
| tenbrains analyze --provider mock --text "..." --learn --minutes 10 --ratings -
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Account takeaways (supply recent posts, or fetch from X with a token):
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
tenbrains takeaway follow levelsio
|
|
80
|
+
tenbrains takeaway refresh levelsio --provider mock --posts @recent.json # supplied
|
|
81
|
+
tenbrains takeaway refresh levelsio --count 20 # fetched (needs X token)
|
|
82
|
+
tenbrains takeaway show levelsio --history
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Suggestion feedback loop:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
tenbrains suggest generate # ranks analyzed, un-bookmarked posts vs your saved signal
|
|
89
|
+
tenbrains suggest save sug_... # -> creates a bookmark (meta.bookmarkId)
|
|
90
|
+
tenbrains suggest dismiss sug_... # -> suppressed in future ranking
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Recall:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
tenbrains search "vector databases" --type analysis,bookmark
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Discovery
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
tenbrains manifest # full command tree, flags, providers, error codes, exit codes, db schema
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## House rules for code changes
|
|
106
|
+
|
|
107
|
+
- ESM TypeScript, strict. Relative imports end in `.js`. No `any`. Keep stdout reserved for the
|
|
108
|
+
result envelope — log to stderr via the `Logger`.
|
|
109
|
+
- After changes: `npm run check` (typecheck + biome + tests) must pass.
|
|
110
|
+
- Add a migration (never edit an existing one) in `src/db/migrations.ts` for schema changes.
|
|
111
|
+
- New persisted outcomes follow the pattern: zod schema → repository method → command returning a
|
|
112
|
+
`CommandResult`. Add a test under `test/`.
|
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# tenbrains
|
|
2
|
+
|
|
3
|
+
An **agent-first** CLI for X research. It analyzes posts, tracks followed accounts, surfaces
|
|
4
|
+
suggestions, and **persists every outcome to a local SQLite database** — all from the command line,
|
|
5
|
+
with no `.env` files and no hosted backend.
|
|
6
|
+
|
|
7
|
+
This is a ground-up rebuild of [moeghashim/tenbrains](https://github.com/moeghashim/tenbrains)
|
|
8
|
+
(originally a Convex-backed web workspace) reshaped around a single design goal: **be consumed by
|
|
9
|
+
agents.** Every command emits one stable JSON envelope on stdout, every failure carries a
|
|
10
|
+
machine-readable code and a deterministic exit status, and the entire surface is discoverable via
|
|
11
|
+
`tenbrains manifest`.
|
|
12
|
+
|
|
13
|
+
## Why it's "agent-first"
|
|
14
|
+
|
|
15
|
+
- **Structured output by default.** stdout is always exactly one JSON envelope. Diagnostics and
|
|
16
|
+
progress go to stderr, so a parser never has to untangle the two. `--pretty` switches to
|
|
17
|
+
human-readable output for terminal use.
|
|
18
|
+
- **Stable contract.** `{ ok, command, data, meta }` on success, `{ ok, command, error }` on
|
|
19
|
+
failure. Error `code`s and process exit codes are fixed per failure class.
|
|
20
|
+
- **Self-describing.** `tenbrains manifest` returns the full command tree, flags, provider catalog,
|
|
21
|
+
error codes, and exit codes as JSON — an agent can discover the whole tool in one call.
|
|
22
|
+
- **Non-interactive.** No blocking prompts. Content comes in via flags, `@file`, or `-` (stdin);
|
|
23
|
+
credentials are collected through commands, never by hand-editing a dotfile.
|
|
24
|
+
- **Everything persisted.** Posts, analyses, takeaways, bookmarks, suggestions, and learning tracks
|
|
25
|
+
all land in one local SQLite file you can point anywhere with `--db`.
|
|
26
|
+
- **Offline-capable.** The built-in `mock` provider produces deterministic analysis with no network,
|
|
27
|
+
so agents and CI can exercise the full pipeline without API keys.
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Node.js **>= 24** (uses the built-in `node:sqlite` — no native modules to compile).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
npm run build
|
|
38
|
+
node dist/bin/tenbrains.js --help # or: npm link -> tenbrains --help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
During development you can run straight from TypeScript:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run dev -- analyze --provider mock --text "hello world"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 1. Configure a provider once (stored in ~/.config/tenbrains/config.json, mode 0600).
|
|
51
|
+
# Optionally add an X API Bearer token in the same step (see "Fetching from X").
|
|
52
|
+
tenbrains setup --provider anthropic --api-key sk-ant-... --default
|
|
53
|
+
|
|
54
|
+
# 2a. Analyze a post you already have (paste the content).
|
|
55
|
+
tenbrains analyze --author levelsio --id 1790000000000000000 \
|
|
56
|
+
--text "Shipping an agent-first CLI today. Everything persists to SQLite, nothing in env files."
|
|
57
|
+
|
|
58
|
+
# 2b. Or analyze a tweet by URL — fetched free via X's oEmbed endpoint, no key needed.
|
|
59
|
+
tenbrains analyze --url "https://x.com/jack/status/20"
|
|
60
|
+
|
|
61
|
+
# 3. Read it back / explore.
|
|
62
|
+
tenbrains analyze list --limit 5
|
|
63
|
+
tenbrains search "agent cli"
|
|
64
|
+
tenbrains db stats --pretty
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
No API key handy? Use the deterministic offline provider:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
tenbrains analyze --provider mock --text "vector databases power semantic search"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## The output contract
|
|
74
|
+
|
|
75
|
+
Success:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"ok": true,
|
|
80
|
+
"command": "analyze",
|
|
81
|
+
"data": { "post": { "...": "..." }, "analysis": { "topic": "...", "novelConcepts": ["..."] } },
|
|
82
|
+
"meta": { "analysisId": "ana_...", "provider": "anthropic", "model": "claude-sonnet-4-6", "mock": false, "persisted": true }
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Failure:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"ok": false,
|
|
91
|
+
"command": "analyze",
|
|
92
|
+
"error": { "code": "MISSING_CREDENTIALS", "message": "No API key configured for Anthropic Claude...", "retryable": false, "details": { "provider": "anthropic" } }
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Exit codes: `0` success · `2` usage · `3` not found · `4` missing credentials / config ·
|
|
97
|
+
`5` provider error · `6` validation · `7` conflict · `1` internal. The full code→exit map is in
|
|
98
|
+
`tenbrains manifest`.
|
|
99
|
+
|
|
100
|
+
## Commands
|
|
101
|
+
|
|
102
|
+
| Command | Purpose |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `analyze` | Analyze a post (`--text`, or `--url`/`--id` to fetch) into topic, summary, intent, 5 novel concepts. `--thread` analyzes a whole thread as one document; `--learn` also builds a track. |
|
|
105
|
+
| `analyze list` / `analyze get <id>` | Read stored analyses. |
|
|
106
|
+
| `takeaway follow\|unfollow\|list\|refresh\|show` | Track accounts; summarize recent posts (supplied via `--posts` or fetched from X) into snapshots. |
|
|
107
|
+
| `suggest generate\|list\|save\|dismiss\|add` | Rank un-saved posts against your saved signal; save/dismiss feedback. |
|
|
108
|
+
| `bookmark add\|list\|show\|tag\|remove` | Save posts with tags (auto-suggested from analysis) and notes. |
|
|
109
|
+
| `learn generate\|today\|done\|show\|list` | Build 7-day Feynman learning tracks, get today's task, and check off progress. |
|
|
110
|
+
| `search <query>` | Full-text search (SQLite FTS5, BM25-ranked, stemmed) across analyses, takeaways, and bookmarks. |
|
|
111
|
+
| `import x-archive <path>` | Bulk-import your extracted official X archive: likes become bookmarked posts, your tweets become posts. Free, idempotent. |
|
|
112
|
+
| `digest [--days N]` | Markdown recap of analyses, takeaways, and bookmarks saved in the window (default 7 days). |
|
|
113
|
+
| `setup` / `config set\|get\|list\|unset\|path` | Collect and manage provider credentials and defaults. |
|
|
114
|
+
| `record get <id>` | Resolve any record by its prefixed id (`post_`, `ana_`, `acc_`, ...). |
|
|
115
|
+
| `db stats\|migrate\|vacuum\|reindex\|reset` | Inspect and maintain the database. |
|
|
116
|
+
| `manifest` | Emit a machine-readable description of the whole CLI. |
|
|
117
|
+
|
|
118
|
+
Global flags (valid on any command): `--json` (default), `--pretty`, `--quiet`,
|
|
119
|
+
`--db <path>`, `--config-dir <path>`.
|
|
120
|
+
|
|
121
|
+
Input flags accept inline text, `@path` to read a file, or `-` to read stdin:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
echo "long post text..." | tenbrains analyze --provider mock --text -
|
|
125
|
+
tenbrains takeaway refresh levelsio --provider mock --posts @recent.json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Configuration & credentials
|
|
129
|
+
|
|
130
|
+
There is **no `.env`**. The CLI owns credential collection:
|
|
131
|
+
|
|
132
|
+
- `tenbrains setup` collects a key interactively (TTY) or via `--api-key` / `--api-key -` (stdin).
|
|
133
|
+
- `tenbrains config set providers.openai.apiKey sk-...` sets any value directly.
|
|
134
|
+
- `--api-key` / `--provider` / `--model` override per invocation.
|
|
135
|
+
|
|
136
|
+
Values are written to a managed JSON file (`tenbrains config path` shows where) with `0600`
|
|
137
|
+
permissions. Secrets are redacted in `config get`/`config list` unless `--reveal` is passed.
|
|
138
|
+
Resolution precedence: CLI flag → config store. Environment variables are intentionally **not**
|
|
139
|
+
consulted, keeping the credential source explicit and auditable.
|
|
140
|
+
|
|
141
|
+
## Fetching from X
|
|
142
|
+
|
|
143
|
+
By default the agent supplies content via `--text`, but the CLI can also pull tweets itself —
|
|
144
|
+
designed **free-first**:
|
|
145
|
+
|
|
146
|
+
- **Single tweets (`analyze`)** use X's public **oEmbed** endpoint by default: no API key, no paid
|
|
147
|
+
tier. `tenbrains analyze --url "https://x.com/user/status/123"` fetches the tweet text + author
|
|
148
|
+
and analyzes it. Control this with `--fetch auto|oembed|api` (default `auto`).
|
|
149
|
+
- **Account timelines (`takeaway`)** have no free path, so they use the official X API v2 with a
|
|
150
|
+
Bearer token: `tenbrains takeaway refresh <user> --count 20` (omit `--posts` to fetch). Most
|
|
151
|
+
accounts need a **paid X API tier (Basic+)** to read timelines.
|
|
152
|
+
- **Threads (`analyze --thread`)** are analyzed as one document. Supply the parts yourself for free
|
|
153
|
+
(`--thread '["part 1", "part 2"]'`, `@file`, or `-`), or pass bare `--thread` with `--url`/`--id`
|
|
154
|
+
to fetch the author's self-thread via the API (Bearer token; recent search covers ~7 days).
|
|
155
|
+
- **Your own history (`import x-archive`)** needs no API at all: request your account archive at
|
|
156
|
+
X → Settings → "Download an archive of your data", extract the zip, and run
|
|
157
|
+
`tenbrains import x-archive <dir>`. Likes land as bookmarked posts (instant signal for
|
|
158
|
+
`suggest generate`), your tweets as posts. Re-running dedupes on tweet id.
|
|
159
|
+
|
|
160
|
+
Store the token (used for timelines and as the `--fetch api` fallback) during setup or config:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
tenbrains setup --provider anthropic --api-key sk-ant-... --x-bearer "AAAA..." # both at once
|
|
164
|
+
tenbrains config set x.bearerToken "AAAA..." # or just the X token
|
|
165
|
+
tenbrains analyze --url https://x.com/jack/status/20 --x-bearer - < token.txt # or per-call (stdin)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
If your tier can't read a tweet/timeline, the CLI returns a structured `PROVIDER_UNAUTHORIZED` /
|
|
169
|
+
`PROVIDER_RATE_LIMITED` error (exit 5) rather than crashing — fall back to `--text` / `--posts`.
|
|
170
|
+
|
|
171
|
+
## Database
|
|
172
|
+
|
|
173
|
+
A single SQLite file (default `~/.local/share/tenbrains/tenbrains.db`, override with `--db`). Schema
|
|
174
|
+
is versioned and migrated automatically on open. Tables: `posts`, `analyses`, `accounts`,
|
|
175
|
+
`takeaway_snapshots`, `bookmarks`, `suggestions`, `learning_tracks`, plus a trigger-maintained
|
|
176
|
+
FTS5 index (`search_fts`) behind `search` — rebuild it anytime with `tenbrains db reindex`.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
tenbrains --db ./research.db analyze --provider mock --text "..." # isolate a workspace
|
|
180
|
+
tenbrains db stats
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Providers
|
|
184
|
+
|
|
185
|
+
Default is **Anthropic Claude**; `openai`, `google`, and `xai` are supported via `--provider`, and
|
|
186
|
+
`mock` runs offline. See `tenbrains manifest` for the live catalog and default models.
|
|
187
|
+
|
|
188
|
+
## Use as a Claude skill
|
|
189
|
+
|
|
190
|
+
This repo ships an [Agent Skill](https://docs.anthropic.com/en/docs/agents-and-tools/agent-skills)
|
|
191
|
+
in [`skill/`](skill/SKILL.md) so Claude (Claude Code, claude.ai, or the Agent SDK) can drive the CLI
|
|
192
|
+
on your behalf — analyze a post, summarize an account, recall saved research — using the JSON
|
|
193
|
+
contract above. The skill is thin: it points Claude at the CLI and at `tenbrains manifest` for live
|
|
194
|
+
discovery.
|
|
195
|
+
|
|
196
|
+
Install it for your own Claude Code (make the `tenbrains` command available first, then copy the
|
|
197
|
+
skill in):
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
npm link # puts `tenbrains` on PATH
|
|
201
|
+
cp -r skill ~/.claude/skills/tenbrains # personal skill, available in every project
|
|
202
|
+
# or, project-scoped: cp -r skill .claude/skills/tenbrains
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
After installing, ask Claude something like "analyze this X post: …" and it will use the skill.
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npm run typecheck # tsc --noEmit
|
|
211
|
+
npm run lint # biome
|
|
212
|
+
npm test # node:test via tsx
|
|
213
|
+
npm run check # all three
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type AnalysisResult, type TakeawayResult } from "../domain/schemas.js";
|
|
2
|
+
import type { ResolvedProvider } from "./resolve.js";
|
|
3
|
+
export interface AnalysisOutcome {
|
|
4
|
+
result: AnalysisResult;
|
|
5
|
+
mock: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface PostForAnalysis {
|
|
8
|
+
text: string;
|
|
9
|
+
authorUsername?: string | undefined;
|
|
10
|
+
authorName?: string | undefined;
|
|
11
|
+
url?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare function analyzePost(resolved: ResolvedProvider, post: PostForAnalysis): Promise<AnalysisOutcome>;
|
|
14
|
+
export interface TakeawayOutcome {
|
|
15
|
+
result: TakeawayResult;
|
|
16
|
+
mock: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function summarizeAccount(resolved: ResolvedProvider, input: {
|
|
19
|
+
account: {
|
|
20
|
+
username: string;
|
|
21
|
+
name?: string | undefined;
|
|
22
|
+
};
|
|
23
|
+
posts: Array<{
|
|
24
|
+
text: string;
|
|
25
|
+
postedAt?: string | undefined;
|
|
26
|
+
}>;
|
|
27
|
+
}): Promise<TakeawayOutcome>;
|
|
28
|
+
//# sourceMappingURL=analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../src/ai/analyzer.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,cAAc,EAEnB,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAU9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAED,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,eAAe,CAAC,CAkB1B;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE;IACL,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACzD,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC,CAAC;CAC/D,GACA,OAAO,CAAC,eAAe,CAAC,CAkB1B"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { parseOrThrow } from "../core/validate.js";
|
|
2
|
+
import { AnalysisResultSchema, TakeawayResultSchema, } from "../domain/schemas.js";
|
|
3
|
+
import { complete } from "./client.js";
|
|
4
|
+
import { extractJsonObject } from "./json.js";
|
|
5
|
+
import { mockAnalysis, mockTakeaway } from "./mock.js";
|
|
6
|
+
import { accountTakeawaySystemPrompt, accountTakeawayUserPrompt, tweetAnalysisSystemPrompt, tweetAnalysisUserPrompt, } from "./prompts.js";
|
|
7
|
+
export async function analyzePost(resolved, post) {
|
|
8
|
+
if (resolved.provider === "mock") {
|
|
9
|
+
return { result: mockAnalysis(post.text), mock: true };
|
|
10
|
+
}
|
|
11
|
+
const text = await complete({
|
|
12
|
+
provider: resolved.provider,
|
|
13
|
+
model: resolved.model,
|
|
14
|
+
apiKey: resolved.apiKey,
|
|
15
|
+
system: tweetAnalysisSystemPrompt(),
|
|
16
|
+
user: tweetAnalysisUserPrompt(post),
|
|
17
|
+
});
|
|
18
|
+
const result = parseOrThrow(AnalysisResultSchema, extractJsonObject(text), "Provider response did not match the expected analysis shape.", "PROVIDER_BAD_OUTPUT");
|
|
19
|
+
return { result, mock: false };
|
|
20
|
+
}
|
|
21
|
+
export async function summarizeAccount(resolved, input) {
|
|
22
|
+
if (resolved.provider === "mock") {
|
|
23
|
+
return { result: mockTakeaway(input.posts), mock: true };
|
|
24
|
+
}
|
|
25
|
+
const text = await complete({
|
|
26
|
+
provider: resolved.provider,
|
|
27
|
+
model: resolved.model,
|
|
28
|
+
apiKey: resolved.apiKey,
|
|
29
|
+
system: accountTakeawaySystemPrompt(),
|
|
30
|
+
user: accountTakeawayUserPrompt(input),
|
|
31
|
+
});
|
|
32
|
+
const result = parseOrThrow(TakeawayResultSchema, extractJsonObject(text), "Provider response did not match the expected takeaway shape.", "PROVIDER_BAD_OUTPUT");
|
|
33
|
+
return { result, mock: false };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.js","sourceRoot":"","sources":["../../src/ai/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAEL,oBAAoB,EAEpB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,cAAc,CAAC;AAetB,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,QAA0B,EAC1B,IAAqB;IAErB,IAAI,QAAQ,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QACjC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACzD,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC;QAC1B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,MAAM,EAAE,QAAQ,CAAC,MAAgB;QACjC,MAAM,EAAE,yBAAyB,EAAE;QACnC,IAAI,EAAE,uBAAuB,CAAC,IAAI,CAAC;KACpC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,YAAY,CACzB,oBAAoB,EACpB,iBAAiB,CAAC,IAAI,CAAC,EACvB,8DAA8D,EAC9D,qBAAqB,CACtB,CAAC;IACF,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACjC,CAAC;AAOD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,QAA0B,EAC1B,KAGC;IAED,IAAI,QAAQ,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QACjC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC3D,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC;QAC1B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,MAAM,EAAE,QAAQ,CAAC,MAAgB;QACjC,MAAM,EAAE,2BAA2B,EAAE;QACrC,IAAI,EAAE,yBAAyB,CAAC,KAAK,CAAC;KACvC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,YAAY,CACzB,oBAAoB,EACpB,iBAAiB,CAAC,IAAI,CAAC,EACvB,8DAA8D,EAC9D,qBAAqB,CACtB,CAAC;IACF,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProviderId } from "./providers.js";
|
|
2
|
+
export interface CompletionRequest {
|
|
3
|
+
provider: Exclude<ProviderId, "mock">;
|
|
4
|
+
model: string;
|
|
5
|
+
apiKey: string;
|
|
6
|
+
system: string;
|
|
7
|
+
user: string;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Call a real provider and return the raw text of its first response message. */
|
|
11
|
+
export declare function complete(req: CompletionRequest): Promise<string>;
|
|
12
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/ai/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqID,kFAAkF;AAClF,wBAAsB,QAAQ,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2CtE"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { CliError } from "../core/errors.js";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
3
|
+
const MAX_TOKENS = 1024;
|
|
4
|
+
function asRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
6
|
+
}
|
|
7
|
+
const ADAPTERS = {
|
|
8
|
+
anthropic: {
|
|
9
|
+
url: () => "https://api.anthropic.com/v1/messages",
|
|
10
|
+
headers: (req) => ({
|
|
11
|
+
"content-type": "application/json",
|
|
12
|
+
"x-api-key": req.apiKey,
|
|
13
|
+
"anthropic-version": "2023-06-01",
|
|
14
|
+
}),
|
|
15
|
+
body: (req) => ({
|
|
16
|
+
model: req.model,
|
|
17
|
+
max_tokens: MAX_TOKENS,
|
|
18
|
+
system: req.system,
|
|
19
|
+
messages: [{ role: "user", content: req.user }],
|
|
20
|
+
}),
|
|
21
|
+
extract: (payload) => {
|
|
22
|
+
const content = asRecord(payload).content;
|
|
23
|
+
if (!Array.isArray(content)) {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
return content
|
|
27
|
+
.map((part) => {
|
|
28
|
+
const record = asRecord(part);
|
|
29
|
+
return record.type === "text" && typeof record.text === "string" ? record.text : "";
|
|
30
|
+
})
|
|
31
|
+
.join("\n")
|
|
32
|
+
.trim();
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
openai: openAiCompatible("https://api.openai.com/v1/chat/completions"),
|
|
36
|
+
xai: openAiCompatible("https://api.x.ai/v1/chat/completions"),
|
|
37
|
+
google: {
|
|
38
|
+
url: (req) => `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(req.model)}:generateContent`,
|
|
39
|
+
headers: (req) => ({
|
|
40
|
+
"content-type": "application/json",
|
|
41
|
+
"x-goog-api-key": req.apiKey,
|
|
42
|
+
}),
|
|
43
|
+
body: (req) => ({
|
|
44
|
+
systemInstruction: { parts: [{ text: req.system }] },
|
|
45
|
+
contents: [{ role: "user", parts: [{ text: req.user }] }],
|
|
46
|
+
generationConfig: { responseMimeType: "application/json" },
|
|
47
|
+
}),
|
|
48
|
+
extract: (payload) => {
|
|
49
|
+
const candidates = asRecord(payload).candidates;
|
|
50
|
+
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
const parts = asRecord(asRecord(candidates[0]).content).parts;
|
|
54
|
+
if (!Array.isArray(parts)) {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
return parts
|
|
58
|
+
.map((part) => {
|
|
59
|
+
const text = asRecord(part).text;
|
|
60
|
+
return typeof text === "string" ? text : "";
|
|
61
|
+
})
|
|
62
|
+
.join("\n")
|
|
63
|
+
.trim();
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
function openAiCompatible(url) {
|
|
68
|
+
return {
|
|
69
|
+
url: () => url,
|
|
70
|
+
headers: (req) => ({
|
|
71
|
+
"content-type": "application/json",
|
|
72
|
+
authorization: `Bearer ${req.apiKey}`,
|
|
73
|
+
}),
|
|
74
|
+
body: (req) => ({
|
|
75
|
+
model: req.model,
|
|
76
|
+
messages: [
|
|
77
|
+
{ role: "system", content: req.system },
|
|
78
|
+
{ role: "user", content: req.user },
|
|
79
|
+
],
|
|
80
|
+
}),
|
|
81
|
+
extract: (payload) => {
|
|
82
|
+
const choices = asRecord(payload).choices;
|
|
83
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
const message = asRecord(asRecord(choices[0]).message);
|
|
87
|
+
return typeof message.content === "string" ? message.content.trim() : "";
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function errorMessageFrom(payload, fallback) {
|
|
92
|
+
const record = asRecord(payload);
|
|
93
|
+
const error = record.error;
|
|
94
|
+
if (typeof error === "string") {
|
|
95
|
+
return error;
|
|
96
|
+
}
|
|
97
|
+
const message = asRecord(error).message ?? record.message;
|
|
98
|
+
return typeof message === "string" && message.trim() ? message.trim() : fallback;
|
|
99
|
+
}
|
|
100
|
+
function mapStatus(provider, status, message) {
|
|
101
|
+
if (status === 401 || status === 403) {
|
|
102
|
+
return new CliError("PROVIDER_UNAUTHORIZED", `${provider}: ${message}`, {
|
|
103
|
+
details: { status },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (status === 429) {
|
|
107
|
+
return new CliError("PROVIDER_RATE_LIMITED", `${provider}: ${message}`, {
|
|
108
|
+
details: { status },
|
|
109
|
+
retryable: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return new CliError("PROVIDER_UPSTREAM", `${provider}: ${message}`, {
|
|
113
|
+
details: { status },
|
|
114
|
+
retryable: status >= 500,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Call a real provider and return the raw text of its first response message. */
|
|
118
|
+
export async function complete(req) {
|
|
119
|
+
const adapter = ADAPTERS[req.provider];
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timeout = setTimeout(() => controller.abort(), req.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
122
|
+
let response;
|
|
123
|
+
try {
|
|
124
|
+
response = await fetch(adapter.url(req), {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: adapter.headers(req),
|
|
127
|
+
body: JSON.stringify(adapter.body(req)),
|
|
128
|
+
signal: controller.signal,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
throw new CliError("PROVIDER_NETWORK", controller.signal.aborted
|
|
133
|
+
? `${req.provider}: request timed out after ${req.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`
|
|
134
|
+
: `${req.provider}: ${error instanceof Error ? error.message : "network error"}`, { retryable: true, cause: error });
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
}
|
|
139
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
140
|
+
const payload = contentType.includes("application/json")
|
|
141
|
+
? await response.json()
|
|
142
|
+
: await response.text();
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
throw mapStatus(req.provider, response.status, errorMessageFrom(payload, `request failed with status ${response.status}`));
|
|
145
|
+
}
|
|
146
|
+
const text = adapter.extract(payload);
|
|
147
|
+
if (!text) {
|
|
148
|
+
throw new CliError("PROVIDER_BAD_OUTPUT", `${req.provider}: empty response from provider.`);
|
|
149
|
+
}
|
|
150
|
+
return text;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/ai/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a single JSON object from a model response. Tolerates leading prose
|
|
3
|
+
* and ```json fences, then balances braces while respecting string literals so
|
|
4
|
+
* a `}` inside a quoted value does not terminate the scan early.
|
|
5
|
+
*/
|
|
6
|
+
export declare function extractJsonObject(text: string): unknown;
|
|
7
|
+
//# sourceMappingURL=json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../src/ai/json.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAuCvD"}
|
package/dist/ai/json.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { CliError } from "../core/errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extract a single JSON object from a model response. Tolerates leading prose
|
|
4
|
+
* and ```json fences, then balances braces while respecting string literals so
|
|
5
|
+
* a `}` inside a quoted value does not terminate the scan early.
|
|
6
|
+
*/
|
|
7
|
+
export function extractJsonObject(text) {
|
|
8
|
+
const fenced = text.replace(/```(?:json)?/gi, "```");
|
|
9
|
+
const start = fenced.indexOf("{");
|
|
10
|
+
if (start === -1) {
|
|
11
|
+
throw badOutput(text);
|
|
12
|
+
}
|
|
13
|
+
let depth = 0;
|
|
14
|
+
let inString = false;
|
|
15
|
+
let escaped = false;
|
|
16
|
+
for (let i = start; i < fenced.length; i += 1) {
|
|
17
|
+
const ch = fenced[i];
|
|
18
|
+
if (inString) {
|
|
19
|
+
if (escaped) {
|
|
20
|
+
escaped = false;
|
|
21
|
+
}
|
|
22
|
+
else if (ch === "\\") {
|
|
23
|
+
escaped = true;
|
|
24
|
+
}
|
|
25
|
+
else if (ch === '"') {
|
|
26
|
+
inString = false;
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ch === '"') {
|
|
31
|
+
inString = true;
|
|
32
|
+
}
|
|
33
|
+
else if (ch === "{") {
|
|
34
|
+
depth += 1;
|
|
35
|
+
}
|
|
36
|
+
else if (ch === "}") {
|
|
37
|
+
depth -= 1;
|
|
38
|
+
if (depth === 0) {
|
|
39
|
+
const candidate = fenced.slice(start, i + 1);
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(candidate);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw badOutput(text, error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw badOutput(text);
|
|
50
|
+
}
|
|
51
|
+
function badOutput(text, cause) {
|
|
52
|
+
return new CliError("PROVIDER_BAD_OUTPUT", "Provider did not return a parseable JSON object.", {
|
|
53
|
+
details: { sample: text.slice(0, 280) },
|
|
54
|
+
cause,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/ai/json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE7C;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;IACrD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;QACjB,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAED,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,GAAG,KAAK,CAAC;YAClB,CAAC;iBAAM,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;gBACvB,OAAO,GAAG,IAAI,CAAC;YACjB,CAAC;iBAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBACtB,QAAQ,GAAG,KAAK,CAAC;YACnB,CAAC;YACD,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,QAAQ,GAAG,IAAI,CAAC;QAClB,CAAC;aAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACtB,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;aAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACtB,KAAK,IAAI,CAAC,CAAC;YACX,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;gBAChB,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC7C,IAAI,CAAC;oBACH,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;gBAC/B,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAC/B,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,SAAS,SAAS,CAAC,IAAY,EAAE,KAAe;IAC9C,OAAO,IAAI,QAAQ,CAAC,qBAAqB,EAAE,kDAAkD,EAAE;QAC7F,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;QACvC,KAAK;KACN,CAAC,CAAC;AACL,CAAC"}
|