@remnic/connector-x 9.69.64
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 +21 -0
- package/README.md +172 -0
- package/dist/chunk-JR2ZNAYD.js +1482 -0
- package/dist/chunk-JR2ZNAYD.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +129 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +467 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Joshua Warren
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @remnic/connector-x
|
|
2
|
+
|
|
3
|
+
X (Twitter) connector for [Remnic](https://github.com/joshuaswarren/remnic) —
|
|
4
|
+
remember the user's own X posts and bookmarks (issue #2009).
|
|
5
|
+
|
|
6
|
+
Bookmarks are deliberate curation; posts are deliberate expression. Both are
|
|
7
|
+
high-trust memory sources. This connector ingests them through **pluggable
|
|
8
|
+
sources** and maps them to Remnic memories with dedupe, provenance, and
|
|
9
|
+
trust gating.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npm install @remnic/connector-x # à-la-carte; @remnic/core works alone without it
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Cost reality — read this first
|
|
16
|
+
|
|
17
|
+
The official X MCP (`https://api.x.com/mcp`) is a transport over the same
|
|
18
|
+
**metered X API**: every read consumes the same quota and pay-per-use credits
|
|
19
|
+
as a raw v2 call. A `tools/call get_users_bookmarks` against an exhausted
|
|
20
|
+
account returns `{"detail":"credits depleted","status":402}` (session
|
|
21
|
+
`initialize`/`tools/list` stay free).
|
|
22
|
+
|
|
23
|
+
The connector is budget-aware by design:
|
|
24
|
+
|
|
25
|
+
- `maxPagesPerSync` (default 2) caps paid pages per cycle.
|
|
26
|
+
- `maxCostUsdPerMonth` (default $1.00) is a hard ceiling; when projected
|
|
27
|
+
spend would cross it, the source **skips the cycle cleanly** (`skipped:
|
|
28
|
+
monthly-cost-cap`) instead of erroring.
|
|
29
|
+
- `costPerReadUsd` (default $0.01 — ~1 credit/read pay-per-use reference
|
|
30
|
+
rate) converts reads to dollars; set it to your account's real rate.
|
|
31
|
+
- Stop-on-known-ids: paging stops as soon as a page contains only posts
|
|
32
|
+
already ingested, so slow-changing bookmarks cost one page per sync.
|
|
33
|
+
- Bookmarks change slowly — keep `syncSchedule` at `3x-daily` or slower.
|
|
34
|
+
|
|
35
|
+
Because reads cost money, the source layer is pluggable so the same
|
|
36
|
+
normalized records can come from zero-credit inputs, cheapest first:
|
|
37
|
+
|
|
38
|
+
| kind | credits | what it reads |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `corpusDir` | 0 | `*.json` files in a local corpus directory (e.g. one exported by another pipeline) |
|
|
41
|
+
| `cli` | 0 | a cookie-GraphQL CLI such as `bird` (`bird bookmarks --json`) |
|
|
42
|
+
| `mcp` | paid | the official X MCP — canonical shape |
|
|
43
|
+
|
|
44
|
+
## Config
|
|
45
|
+
|
|
46
|
+
The `xConnector` block (issue #2009). Put it in a JSON file and point the
|
|
47
|
+
CLI at it, or pass the parsed object to `parseXConnectorConfig` from a host:
|
|
48
|
+
|
|
49
|
+
```jsonc
|
|
50
|
+
{
|
|
51
|
+
"xConnector": {
|
|
52
|
+
"enabled": true,
|
|
53
|
+
"userId": "123456789", // numeric X user id; enables own-post ingestion
|
|
54
|
+
"sources": [
|
|
55
|
+
{ "id": "local-corpus", "kind": "corpusDir", "path": "~/corpus/bookmarks" },
|
|
56
|
+
{ "id": "bird", "kind": "cli", "bin": "bird" },
|
|
57
|
+
{
|
|
58
|
+
"id": "x-mcp", "kind": "mcp", "url": "https://api.x.com/mcp",
|
|
59
|
+
"auth": { "tokenFile": "~/.openclaw/secrets/x-tokens.json" },
|
|
60
|
+
"bookmarksTool": "get_users_bookmarks", // defaults shown; override if X renames
|
|
61
|
+
"timelineTool": "get_users_tweets",
|
|
62
|
+
"maxResults": 20,
|
|
63
|
+
"budget": { "maxPagesPerSync": 2, "maxCostUsdPerMonth": 1.0, "costPerReadUsd": 0.01 }
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"sourcePriority": ["local-corpus", "bird", "x-mcp"], // cheapest first
|
|
67
|
+
"syncSchedule": "3x-daily",
|
|
68
|
+
"memoryMode": "suggest", // "suggest" → review queue, "store" → direct write
|
|
69
|
+
"stateDir": "~/.remnic/x-connector"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Invalid values are rejected, never silently reinterpreted: unknown kinds,
|
|
75
|
+
duplicate source ids, priorities naming unknown sources, non-numeric
|
|
76
|
+
`userId`, and bad enum values all throw `XConfigError`.
|
|
77
|
+
|
|
78
|
+
## Auth setup (MCP source)
|
|
79
|
+
|
|
80
|
+
Bookmarks need a **user-context OAuth2 token** — app-only bearers have no
|
|
81
|
+
user context. There is no OAuth discovery or dynamic registration on
|
|
82
|
+
`api.x.com/mcp`; you need a pre-registered confidential client:
|
|
83
|
+
|
|
84
|
+
1. Create an OAuth2 client in the X developer portal.
|
|
85
|
+
2. Run the authorization-code flow once (with offline access) and write the
|
|
86
|
+
token file:
|
|
87
|
+
```json
|
|
88
|
+
{ "access_token": "...", "refresh_token": "...", "expires_at": 1750000000000 }
|
|
89
|
+
```
|
|
90
|
+
`expires_at` is epoch milliseconds. Unknown extra fields are preserved.
|
|
91
|
+
3. Give the connector the client credentials (env is easiest):
|
|
92
|
+
`REMNIC_X_CLIENT_ID`, `REMNIC_X_CLIENT_SECRET`.
|
|
93
|
+
|
|
94
|
+
### Single-owner refresh chain (important)
|
|
95
|
+
|
|
96
|
+
X **rotates the refresh token on every refresh**. Two independent
|
|
97
|
+
refreshers fork the chain and kill one of them — you get HTTP 401 on the
|
|
98
|
+
next refresh and must re-authorize. `XTokenStore` is built to be the single
|
|
99
|
+
owner: refreshes only run while holding `<tokenFile>.lock`; a concurrent
|
|
100
|
+
refresher waits, then adopts the rotated pair from the file. Do not run
|
|
101
|
+
another tool that refreshes the same grant.
|
|
102
|
+
|
|
103
|
+
The token file is written 0600, atomically. A 400/401/403 on refresh raises
|
|
104
|
+
`XRefreshChainBrokenError` with recovery instructions.
|
|
105
|
+
|
|
106
|
+
## Ingestion → memory mapping
|
|
107
|
+
|
|
108
|
+
Every source emits the same normalized record
|
|
109
|
+
(`postId`, `kind: bookmark|own_post`, `author`, `createdAt`, `text`, `urls`,
|
|
110
|
+
`mediaCount`, optional `enrichment`), deduped by `postId` + content
|
|
111
|
+
fingerprint — re-fetching a bookmark daily never churns the memory store,
|
|
112
|
+
and an edited post re-ingests exactly once.
|
|
113
|
+
|
|
114
|
+
| record | tags | category | confidence |
|
|
115
|
+
|---|---|---|---|
|
|
116
|
+
| bookmark | `x/bookmark` | `reference` (has URL) or `interest` | 0.7 |
|
|
117
|
+
| own post | `x/post` | `expression` | 0.9 |
|
|
118
|
+
|
|
119
|
+
Author usernames become `person-<handle>` entity refs, feeding the entity
|
|
120
|
+
graph. `memoryMode` gates trust: `suggest` (default) routes through
|
|
121
|
+
`XMemorySink.submitSuggestion` (review queue); `store` routes through
|
|
122
|
+
`storeMemory`. Records carry provenance (`sourceId`, `syncRunId`,
|
|
123
|
+
`fetchedAt`) for attribution.
|
|
124
|
+
|
|
125
|
+
## CLI
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
remnic-x status [--config path] [--json] # offline: sources, availability, spend vs cap
|
|
129
|
+
remnic-x sync [--config path] [--json] # one cycle; skips are expected, not errors
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Default config path: `$REMNIC_X_CONFIG` or `~/.config/remnic/x-connector.json`.
|
|
133
|
+
Exit code 0 for skips (credits depleted, caps), 1 for sink failures, 2 for
|
|
134
|
+
bad invocation or config.
|
|
135
|
+
|
|
136
|
+
## Host API
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { parseXConnectorConfig, runXSync, getXStatus } from "@remnic/connector-x";
|
|
140
|
+
|
|
141
|
+
const config = parseXConnectorConfig(rawBlock);
|
|
142
|
+
const report = await runXSync(config, {
|
|
143
|
+
sink: {
|
|
144
|
+
submitSuggestion: (s) => myReviewQueue.push(s),
|
|
145
|
+
storeMemory: async (s) => { await memoryStore.write(toMemory(s)); },
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
const status = await getXStatus(config); // offline, zero credits
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The default on-disk sink (`createFileSink`) writes `suggest`-mode files to
|
|
152
|
+
`<stateDir>/suggestions/` and `store`-mode files to `<stateDir>/records/`;
|
|
153
|
+
every ingested record is also materialized under `<stateDir>/records/`.
|
|
154
|
+
|
|
155
|
+
## Non-goals
|
|
156
|
+
|
|
157
|
+
- No posting, liking, or bookmark writes (X blocks autonomous posting via
|
|
158
|
+
MCP; writes are priced per action).
|
|
159
|
+
- No full-archive backfill through the metered MCP — backfill from a local
|
|
160
|
+
corpus.
|
|
161
|
+
- No scraping: zero-credit sources are pluggable inputs provided by the
|
|
162
|
+
deployment.
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
npm run test # node:test via tsx, no network
|
|
168
|
+
npm run check-types
|
|
169
|
+
npm run build # tsup → dist/
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Tests run fully offline against scripted HTTP fakes and temp directories.
|