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.
Files changed (183) hide show
  1. package/AGENTS.md +112 -0
  2. package/README.md +218 -0
  3. package/dist/ai/analyzer.d.ts +28 -0
  4. package/dist/ai/analyzer.d.ts.map +1 -0
  5. package/dist/ai/analyzer.js +35 -0
  6. package/dist/ai/analyzer.js.map +1 -0
  7. package/dist/ai/client.d.ts +12 -0
  8. package/dist/ai/client.d.ts.map +1 -0
  9. package/dist/ai/client.js +152 -0
  10. package/dist/ai/client.js.map +1 -0
  11. package/dist/ai/json.d.ts +7 -0
  12. package/dist/ai/json.d.ts.map +1 -0
  13. package/dist/ai/json.js +57 -0
  14. package/dist/ai/json.js.map +1 -0
  15. package/dist/ai/mock.d.ts +6 -0
  16. package/dist/ai/mock.d.ts.map +1 -0
  17. package/dist/ai/mock.js +66 -0
  18. package/dist/ai/mock.js.map +1 -0
  19. package/dist/ai/prompts.d.ts +19 -0
  20. package/dist/ai/prompts.d.ts.map +1 -0
  21. package/dist/ai/prompts.js +40 -0
  22. package/dist/ai/prompts.js.map +1 -0
  23. package/dist/ai/providers.d.ts +22 -0
  24. package/dist/ai/providers.d.ts.map +1 -0
  25. package/dist/ai/providers.js +56 -0
  26. package/dist/ai/providers.js.map +1 -0
  27. package/dist/ai/resolve.d.ts +24 -0
  28. package/dist/ai/resolve.d.ts.map +1 -0
  29. package/dist/ai/resolve.js +33 -0
  30. package/dist/ai/resolve.js.map +1 -0
  31. package/dist/bin/preflight.d.ts +2 -0
  32. package/dist/bin/preflight.d.ts.map +1 -0
  33. package/dist/bin/preflight.js +11 -0
  34. package/dist/bin/preflight.js.map +1 -0
  35. package/dist/bin/tenbrains.d.ts +3 -0
  36. package/dist/bin/tenbrains.d.ts.map +1 -0
  37. package/dist/bin/tenbrains.js +9 -0
  38. package/dist/bin/tenbrains.js.map +1 -0
  39. package/dist/cli.d.ts +4 -0
  40. package/dist/cli.d.ts.map +1 -0
  41. package/dist/cli.js +554 -0
  42. package/dist/cli.js.map +1 -0
  43. package/dist/commands/analyze.d.ts +7 -0
  44. package/dist/commands/analyze.d.ts.map +1 -0
  45. package/dist/commands/analyze.js +204 -0
  46. package/dist/commands/analyze.js.map +1 -0
  47. package/dist/commands/bookmark.d.ts +9 -0
  48. package/dist/commands/bookmark.d.ts.map +1 -0
  49. package/dist/commands/bookmark.js +120 -0
  50. package/dist/commands/bookmark.js.map +1 -0
  51. package/dist/commands/config.d.ts +15 -0
  52. package/dist/commands/config.d.ts.map +1 -0
  53. package/dist/commands/config.js +142 -0
  54. package/dist/commands/config.js.map +1 -0
  55. package/dist/commands/db.d.ts +9 -0
  56. package/dist/commands/db.d.ts.map +1 -0
  57. package/dist/commands/db.js +64 -0
  58. package/dist/commands/db.js.map +1 -0
  59. package/dist/commands/digest.d.ts +19 -0
  60. package/dist/commands/digest.d.ts.map +1 -0
  61. package/dist/commands/digest.js +71 -0
  62. package/dist/commands/digest.js.map +1 -0
  63. package/dist/commands/import.d.ts +11 -0
  64. package/dist/commands/import.d.ts.map +1 -0
  65. package/dist/commands/import.js +78 -0
  66. package/dist/commands/import.js.map +1 -0
  67. package/dist/commands/learn.d.ts +9 -0
  68. package/dist/commands/learn.d.ts.map +1 -0
  69. package/dist/commands/learn.js +149 -0
  70. package/dist/commands/learn.js.map +1 -0
  71. package/dist/commands/manifest.d.ts +13 -0
  72. package/dist/commands/manifest.d.ts.map +1 -0
  73. package/dist/commands/manifest.js +62 -0
  74. package/dist/commands/manifest.js.map +1 -0
  75. package/dist/commands/record.d.ts +10 -0
  76. package/dist/commands/record.d.ts.map +1 -0
  77. package/dist/commands/record.js +37 -0
  78. package/dist/commands/record.js.map +1 -0
  79. package/dist/commands/search.d.ts +5 -0
  80. package/dist/commands/search.d.ts.map +1 -0
  81. package/dist/commands/search.js +33 -0
  82. package/dist/commands/search.js.map +1 -0
  83. package/dist/commands/shared.d.ts +9 -0
  84. package/dist/commands/shared.d.ts.map +1 -0
  85. package/dist/commands/shared.js +13 -0
  86. package/dist/commands/shared.js.map +1 -0
  87. package/dist/commands/suggest.d.ts +9 -0
  88. package/dist/commands/suggest.d.ts.map +1 -0
  89. package/dist/commands/suggest.js +102 -0
  90. package/dist/commands/suggest.js.map +1 -0
  91. package/dist/commands/takeaway.d.ts +9 -0
  92. package/dist/commands/takeaway.d.ts.map +1 -0
  93. package/dist/commands/takeaway.js +167 -0
  94. package/dist/commands/takeaway.js.map +1 -0
  95. package/dist/core/config.d.ts +52 -0
  96. package/dist/core/config.d.ts.map +1 -0
  97. package/dist/core/config.js +180 -0
  98. package/dist/core/config.js.map +1 -0
  99. package/dist/core/context.d.ts +28 -0
  100. package/dist/core/context.d.ts.map +1 -0
  101. package/dist/core/context.js +40 -0
  102. package/dist/core/context.js.map +1 -0
  103. package/dist/core/errors.d.ts +33 -0
  104. package/dist/core/errors.d.ts.map +1 -0
  105. package/dist/core/errors.js +82 -0
  106. package/dist/core/errors.js.map +1 -0
  107. package/dist/core/input.d.ts +9 -0
  108. package/dist/core/input.d.ts.map +1 -0
  109. package/dist/core/input.js +47 -0
  110. package/dist/core/input.js.map +1 -0
  111. package/dist/core/opts.d.ts +8 -0
  112. package/dist/core/opts.d.ts.map +1 -0
  113. package/dist/core/opts.js +38 -0
  114. package/dist/core/opts.js.map +1 -0
  115. package/dist/core/output.d.ts +58 -0
  116. package/dist/core/output.d.ts.map +1 -0
  117. package/dist/core/output.js +76 -0
  118. package/dist/core/output.js.map +1 -0
  119. package/dist/core/paths.d.ts +20 -0
  120. package/dist/core/paths.d.ts.map +1 -0
  121. package/dist/core/paths.js +31 -0
  122. package/dist/core/paths.js.map +1 -0
  123. package/dist/core/text.d.ts +10 -0
  124. package/dist/core/text.d.ts.map +1 -0
  125. package/dist/core/text.js +132 -0
  126. package/dist/core/text.js.map +1 -0
  127. package/dist/core/validate.d.ts +5 -0
  128. package/dist/core/validate.d.ts.map +1 -0
  129. package/dist/core/validate.js +17 -0
  130. package/dist/core/validate.js.map +1 -0
  131. package/dist/db/database.d.ts +27 -0
  132. package/dist/db/database.d.ts.map +1 -0
  133. package/dist/db/database.js +90 -0
  134. package/dist/db/database.js.map +1 -0
  135. package/dist/db/fts.d.ts +12 -0
  136. package/dist/db/fts.d.ts.map +1 -0
  137. package/dist/db/fts.js +42 -0
  138. package/dist/db/fts.js.map +1 -0
  139. package/dist/db/migrations.d.ts +20 -0
  140. package/dist/db/migrations.d.ts.map +1 -0
  141. package/dist/db/migrations.js +243 -0
  142. package/dist/db/migrations.js.map +1 -0
  143. package/dist/db/repositories.d.ts +130 -0
  144. package/dist/db/repositories.d.ts.map +1 -0
  145. package/dist/db/repositories.js +418 -0
  146. package/dist/db/repositories.js.map +1 -0
  147. package/dist/domain/learn.d.ts +25 -0
  148. package/dist/domain/learn.d.ts.map +1 -0
  149. package/dist/domain/learn.js +85 -0
  150. package/dist/domain/learn.js.map +1 -0
  151. package/dist/domain/schemas.d.ts +169 -0
  152. package/dist/domain/schemas.d.ts.map +1 -0
  153. package/dist/domain/schemas.js +54 -0
  154. package/dist/domain/schemas.js.map +1 -0
  155. package/dist/domain/search.d.ts +32 -0
  156. package/dist/domain/search.d.ts.map +1 -0
  157. package/dist/domain/search.js +77 -0
  158. package/dist/domain/search.js.map +1 -0
  159. package/dist/domain/suggest.d.ts +17 -0
  160. package/dist/domain/suggest.d.ts.map +1 -0
  161. package/dist/domain/suggest.js +114 -0
  162. package/dist/domain/suggest.js.map +1 -0
  163. package/dist/domain/tags.d.ts +9 -0
  164. package/dist/domain/tags.d.ts.map +1 -0
  165. package/dist/domain/tags.js +40 -0
  166. package/dist/domain/tags.js.map +1 -0
  167. package/dist/domain/types.d.ts +84 -0
  168. package/dist/domain/types.d.ts.map +1 -0
  169. package/dist/domain/types.js +2 -0
  170. package/dist/domain/types.js.map +1 -0
  171. package/dist/index.d.ts +19 -0
  172. package/dist/index.d.ts.map +1 -0
  173. package/dist/index.js +18 -0
  174. package/dist/index.js.map +1 -0
  175. package/dist/x/archive.d.ts +32 -0
  176. package/dist/x/archive.d.ts.map +1 -0
  177. package/dist/x/archive.js +108 -0
  178. package/dist/x/archive.js.map +1 -0
  179. package/dist/x/client.d.ts +61 -0
  180. package/dist/x/client.d.ts.map +1 -0
  181. package/dist/x/client.js +321 -0
  182. package/dist/x/client.js.map +1 -0
  183. 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"}
@@ -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"}