@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 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.