unhoard 0.1.0__tar.gz

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 (80) hide show
  1. unhoard-0.1.0/LICENSE +176 -0
  2. unhoard-0.1.0/MANIFEST.in +1 -0
  3. unhoard-0.1.0/PKG-INFO +328 -0
  4. unhoard-0.1.0/README.md +299 -0
  5. unhoard-0.1.0/pyproject.toml +52 -0
  6. unhoard-0.1.0/setup.cfg +4 -0
  7. unhoard-0.1.0/src/unhoard/__init__.py +3 -0
  8. unhoard-0.1.0/src/unhoard/__main__.py +3 -0
  9. unhoard-0.1.0/src/unhoard/agents.py +528 -0
  10. unhoard-0.1.0/src/unhoard/cli.py +457 -0
  11. unhoard-0.1.0/src/unhoard/config.py +374 -0
  12. unhoard-0.1.0/src/unhoard/demo.py +147 -0
  13. unhoard-0.1.0/src/unhoard/demo_media/av0.jpg +0 -0
  14. unhoard-0.1.0/src/unhoard/demo_media/av1.jpg +0 -0
  15. unhoard-0.1.0/src/unhoard/demo_media/av2.jpg +0 -0
  16. unhoard-0.1.0/src/unhoard/demo_media/av3.jpg +0 -0
  17. unhoard-0.1.0/src/unhoard/discover.py +295 -0
  18. unhoard-0.1.0/src/unhoard/doctor.py +464 -0
  19. unhoard-0.1.0/src/unhoard/enrich.py +601 -0
  20. unhoard-0.1.0/src/unhoard/firstrun.py +162 -0
  21. unhoard-0.1.0/src/unhoard/launch/__init__.py +122 -0
  22. unhoard-0.1.0/src/unhoard/launch/brief.py +75 -0
  23. unhoard-0.1.0/src/unhoard/launch/command.py +65 -0
  24. unhoard-0.1.0/src/unhoard/launch/omnigent.py +89 -0
  25. unhoard-0.1.0/src/unhoard/launch/printer.py +17 -0
  26. unhoard-0.1.0/src/unhoard/mcp.py +295 -0
  27. unhoard-0.1.0/src/unhoard/netsafe.py +36 -0
  28. unhoard-0.1.0/src/unhoard/omnigent_detect.py +184 -0
  29. unhoard-0.1.0/src/unhoard/phone.py +168 -0
  30. unhoard-0.1.0/src/unhoard/postdata.py +130 -0
  31. unhoard-0.1.0/src/unhoard/present.py +29 -0
  32. unhoard-0.1.0/src/unhoard/procs.py +106 -0
  33. unhoard-0.1.0/src/unhoard/queue.py +200 -0
  34. unhoard-0.1.0/src/unhoard/server.py +347 -0
  35. unhoard-0.1.0/src/unhoard/store.py +174 -0
  36. unhoard-0.1.0/src/unhoard/sync.py +253 -0
  37. unhoard-0.1.0/src/unhoard/tomledit.py +180 -0
  38. unhoard-0.1.0/src/unhoard/tryout.py +70 -0
  39. unhoard-0.1.0/src/unhoard/web/app.css +318 -0
  40. unhoard-0.1.0/src/unhoard/web/app.js +569 -0
  41. unhoard-0.1.0/src/unhoard/web/index.html +57 -0
  42. unhoard-0.1.0/src/unhoard/xauth.py +627 -0
  43. unhoard-0.1.0/src/unhoard.egg-info/PKG-INFO +328 -0
  44. unhoard-0.1.0/src/unhoard.egg-info/SOURCES.txt +78 -0
  45. unhoard-0.1.0/src/unhoard.egg-info/dependency_links.txt +1 -0
  46. unhoard-0.1.0/src/unhoard.egg-info/entry_points.txt +5 -0
  47. unhoard-0.1.0/src/unhoard.egg-info/top_level.txt +2 -0
  48. unhoard-0.1.0/src/unhoard_ext/__init__.py +6 -0
  49. unhoard-0.1.0/src/unhoard_ext/dist/extension.css +13 -0
  50. unhoard-0.1.0/src/unhoard_ext/dist/extension.js +31 -0
  51. unhoard-0.1.0/src/unhoard_ext/plugin.py +37 -0
  52. unhoard-0.1.0/tests/browser_check.py +202 -0
  53. unhoard-0.1.0/tests/fake_agent.py +63 -0
  54. unhoard-0.1.0/tests/fixtures/bookmarks_newer.json +10 -0
  55. unhoard-0.1.0/tests/fixtures/bookmarks_page1.json +20 -0
  56. unhoard-0.1.0/tests/fixtures/bookmarks_page2.json +17 -0
  57. unhoard-0.1.0/tests/fixtures/bookmarks_rich.json +41 -0
  58. unhoard-0.1.0/tests/helpers.py +170 -0
  59. unhoard-0.1.0/tests/test_auto_launch.py +332 -0
  60. unhoard-0.1.0/tests/test_browser.py +121 -0
  61. unhoard-0.1.0/tests/test_cli.py +360 -0
  62. unhoard-0.1.0/tests/test_config_store.py +229 -0
  63. unhoard-0.1.0/tests/test_discover.py +337 -0
  64. unhoard-0.1.0/tests/test_doctor.py +301 -0
  65. unhoard-0.1.0/tests/test_enrich.py +588 -0
  66. unhoard-0.1.0/tests/test_firstrun.py +251 -0
  67. unhoard-0.1.0/tests/test_guard.py +364 -0
  68. unhoard-0.1.0/tests/test_launch.py +321 -0
  69. unhoard-0.1.0/tests/test_live.py +58 -0
  70. unhoard-0.1.0/tests/test_mcp.py +359 -0
  71. unhoard-0.1.0/tests/test_misc.py +151 -0
  72. unhoard-0.1.0/tests/test_omnigent_detect.py +294 -0
  73. unhoard-0.1.0/tests/test_phone.py +499 -0
  74. unhoard-0.1.0/tests/test_presets.py +197 -0
  75. unhoard-0.1.0/tests/test_procs.py +196 -0
  76. unhoard-0.1.0/tests/test_queue.py +155 -0
  77. unhoard-0.1.0/tests/test_server.py +315 -0
  78. unhoard-0.1.0/tests/test_sync.py +307 -0
  79. unhoard-0.1.0/tests/test_tryout.py +210 -0
  80. unhoard-0.1.0/tests/test_xauth.py +810 -0
unhoard-0.1.0/LICENSE ADDED
@@ -0,0 +1,176 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
@@ -0,0 +1 @@
1
+ recursive-include tests *.py *.json
unhoard-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,328 @@
1
+ Metadata-Version: 2.4
2
+ Name: unhoard
3
+ Version: 0.1.0
4
+ Summary: Your X bookmarks aren't a graveyard anymore: your agent reads each one, works out why you saved it, and writes a task you start with one click.
5
+ Author: vicennial
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/vicennial/unhoard
8
+ Project-URL: Repository, https://github.com/vicennial/unhoard
9
+ Project-URL: Issues, https://github.com/vicennial/unhoard/issues
10
+ Keywords: x,twitter,bookmarks,agents,ai-agents,omnigent,claude-code,codex,productivity,cli
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.11
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Dynamic: license-file
29
+
30
+ # unhoard
31
+
32
+ **Made for [Omnigent](https://github.com/omnigent-ai/omnigent). Works with any agent.**
33
+
34
+ Your X bookmarks aren't a graveyard anymore. Your agent reads every bookmark, works out why you saved it, and writes
35
+ a task you can start with one click.
36
+
37
+ unhoard syncs your X bookmarks into a local SQLite file, has your own agent (Claude Code, Codex, or any command that
38
+ reads stdin and prints JSON) write "why you probably saved this" plus up to a few concrete tasks per bookmark, and shows
39
+ them as cards on a page that only listens on your own machine. You press **Go**, **Later** or **Skip**, a few cards a
40
+ day. Nothing starts until you press Go.
41
+
42
+ [Quick start](#quick-start) · [How it works](#how-it-works) · [Why Omnigent first](#why-omnigent-first) ·
43
+ [X API access](#x-api-access) · [Commands](#commands) · [Privacy](#privacy-and-safety) ·
44
+ [Phone](https://github.com/vicennial/unhoard/blob/main/docs/phone.md) · [Card API](https://github.com/vicennial/unhoard/blob/main/docs/card-api.md) · [Agent commands](https://github.com/vicennial/unhoard/blob/main/docs/agent-commands.md)
45
+
46
+ ![unhoard: the card page turns saved posts into tasks, and Go starts one](https://raw.githubusercontent.com/vicennial/unhoard/main/docs/images/demo.gif)
47
+
48
+ > Status: **v0.1, experimental.** Apache-2.0 licensed. Python 3.11 or newer, no dependencies.
49
+
50
+ ## Why
51
+
52
+ Saving a post takes one tap; acting on it takes a decision you never get around to. Bookmarks pile up because the
53
+ "why" is lost the day after. unhoard keeps the part that is cheap for a person (deciding) and hands the expensive part
54
+ to your agent: reading the post, remembering why it mattered to *you*, and turning that into a task with a brief the
55
+ agent can start on.
56
+
57
+ - **It writes the why, not a summary.** A second pass looks at your notes, memory folders and recent repositories
58
+ (only the ones you listed) to connect the post to your actual work.
59
+ - **It is a short queue, not another inbox.** Three cards a day by default; the rest wait.
60
+ - **Safe by construction.** The agent that reads the untrusted post never sees your files, and the agent that sees
61
+ your files never sees the post or the web. See [Privacy and safety](#privacy-and-safety).
62
+ - **Local first.** SQLite on your disk, a server on `127.0.0.1`, your X login in a 0600 file.
63
+
64
+ ## How it works
65
+
66
+ 1. **Sync.** `unhoard sync` pulls your bookmarks from the X API into a local SQLite file (incremental, idempotent).
67
+ 2. **Your agent writes the why and the task.** `unhoard enrich` runs your agent in two passes per bookmark: one reads the
68
+ post, one connects it to your context. Out come "why you probably saved this" and proposed tasks, ranked
69
+ (priority, then confidence, then newest).
70
+ 3. **Press Go.** `unhoard serve` opens the card page. Go starts the task: as a **session in the "X Bookmarks" project in
71
+ Omnigent**, or by **running your own command** with the brief on stdin, or, with neither set up, by **showing the
72
+ brief** so you can paste it anywhere. Later hides the card for a few days; Skip drops it.
73
+
74
+ ![The card page with fictional demo posts: today's three picks, each with a Show brief (Go), Later and Skip button](https://raw.githubusercontent.com/vicennial/unhoard/main/docs/images/cards.png)
75
+
76
+ *The page above is `unhoard try` with the fictional sample data; there the first button says "Show brief" because nothing
77
+ is set up to start a task (with Omnigent or your command it says "Go"). The demo people, posts, numbers and links are made up:
78
+ their handles contain a dot, which X handles cannot, so none of them is a real account, and the portraits are
79
+ AI-generated (Nano Banana Pro) and show nobody real.*
80
+
81
+ ## Quick start
82
+
83
+ Python 3.11 or newer. There are no dependencies.
84
+
85
+ **Look first, with no account and nothing saved on your machine** (uses a temporary folder with fictional sample
86
+ cards, and removes it when you stop):
87
+
88
+ ```bash
89
+ uvx unhoard try
90
+ # or, straight from the repository:
91
+ uvx --from git+https://github.com/vicennial/unhoard unhoard try
92
+ ```
93
+
94
+ **Install:**
95
+
96
+ ```bash
97
+ uv tool install unhoard # or: pipx install unhoard
98
+ # or straight from the repository:
99
+ uv tool install git+https://github.com/vicennial/unhoard # or: pipx install git+https://github.com/vicennial/unhoard
100
+ ```
101
+
102
+ **Use your own bookmarks:**
103
+
104
+ ```bash
105
+ unhoard setup # writes the config, asks a few questions, finds Omnigent, your notes and repos
106
+ unhoard login x # opens X in your browser; needs your own X developer app (see X API access)
107
+ unhoard sync # pull your bookmarks
108
+ unhoard enrich --limit 5 # have your agent write the why and the tasks (a few first, to check the cost)
109
+ unhoard serve # http://127.0.0.1:8765
110
+ ```
111
+
112
+ `unhoard doctor` checks what each enrichment agent can really do (a few tiny agent calls) and what Go will do.
113
+ `unhoard demo` adds the fictional sample cards to your own database if you want to look around before you have synced.
114
+ Run `sync` and `enrich` on a schedule with [cron, a Claude Code `/loop`, or Hermes cron](https://github.com/vicennial/unhoard/tree/main/examples/schedule/).
115
+
116
+ Open <http://127.0.0.1:8765>. Today's picks are a row of stations, each with the post as it looks on X, why you
117
+ probably saved it, the task with its brief and any links in it, a note field and the **Go / Later / Skip** buttons.
118
+ The rest wait in a list below; open any row to answer it in place. It works at phone width (through your own port
119
+ forwarding if you want it; the server only ever listens on loopback). `next` and `decide` do the same in a terminal.
120
+
121
+ ### What Go does
122
+
123
+ Set `[launch] adapter` in `config.toml`. The default is `auto`:
124
+
125
+ - `auto` (default): an Omnigent session when `[omnigent] agent_id` is set **and** the Omnigent API answers on this machine
126
+ when you press the button; otherwise it shows the brief. `unhoard setup` and `unhoard agents` find your local Omnigent
127
+ and save the agent you pick. An explicit adapter below always wins.
128
+ - `omnigent`: always create a session on your Omnigent server with the task brief as the first message
129
+ (`POST {api}/v1/sessions`). Set `[omnigent] agent_id` to a registered agent (`unhoard agents` lists them;
130
+ `host_id`, `workspace`, `harness`, `model` and `reasoning_effort` are optional). Sessions go into the Omnigent
131
+ project (sidebar folder) named by `project`, "X Bookmarks" by default, created on the first Go; `project = ""` for none.
132
+ The session id is shown on the card page under "Launched".
133
+ - `command`: pipe the brief to a program you name (an argv list, detached). This is how any other agent runs your tasks.
134
+ - `print`: show the brief on the page (plain text, with a Copy button) and in the terminal running the server, so
135
+ you can paste it anywhere.
136
+
137
+ The button says **Go** when it starts something (an Omnigent session or your command) and **Show brief** when it only
138
+ shows the brief. If Omnigent starts or stops after the page loaded, the server refuses the click instead of doing
139
+ something else (the page reloads and the button changes; nothing starts and the card stays). With `auto`, an agent
140
+ chosen and Omnigent not running, `unhoard decide ... go` fails and the card goes back to the queue. A failed launch puts the card back in the queue and shows the reason. `status` and `doctor` show the
141
+ configured adapter and the one that applies now.
142
+
143
+ ### From your phone
144
+
145
+ You can start and answer cards from your phone over your own private network. It is opt-in: the server still listens
146
+ on `127.0.0.1` only, and nothing changes unless you set it up. The recommended route is Tailscale:
147
+
148
+ ```bash
149
+ unhoard phone # prints the steps for this machine; changes nothing
150
+ unhoard phone --yes # also writes [server] allowed_hosts to your config
151
+ tailscale serve --bg 8765 # you run this, to share the page with your tailnet (never `funnel`)
152
+ ```
153
+
154
+ ![The brief panel on a phone after pressing Show brief](https://raw.githubusercontent.com/vicennial/unhoard/main/docs/images/phone-brief.png)
155
+
156
+ Anyone on your tailnet can press Go, so share it only with people you trust. An SSH tunnel works with no config
157
+ change. Details and the security notes: [docs/phone.md](https://github.com/vicennial/unhoard/blob/main/docs/phone.md).
158
+
159
+ ## Why Omnigent first
160
+
161
+ [Omnigent](https://github.com/omnigent-ai/omnigent) gives each task a visible, resumable session in one project
162
+ folder ("X Bookmarks"), in the same web UI you already use for your other Omnigent sessions. You press Go, the task
163
+ shows up as a session you can open, watch, steer and come back to later, instead of vanishing into a terminal.
164
+
165
+ Other agents work through the `command` adapter: unhoard starts your program with the brief on stdin, so any agent CLI
166
+ or script that takes a prompt on stdin fits. They get the same brief; they just don't get the session list.
167
+
168
+ ### Coming: an Omnigent sidebar tab
169
+
170
+ A **Bookmarks** entry in the Omnigent sidebar is planned. It needs Omnigent extension API support that does not exist
171
+ yet: starting sessions from an extension page, and reading a local data source. Until then, use the card page.
172
+ `src/unhoard_ext/` holds a scaffold that only explains this.
173
+
174
+ ## X API access
175
+
176
+ You need your own X API access and your own X developer app (OAuth 2.0 enabled; "Native App" is fine). Easiest: set the
177
+ app's callback URL to `http://127.0.0.1:8766/callback`, then run `X_CLIENT_ID=... unhoard login x` and approve it in the
178
+ browser. The login is kept in a 0600 file in the data dir and refreshed automatically; `unhoard logout x` removes it.
179
+ Setup and security notes: [docs/x-login.md](https://github.com/vicennial/unhoard/blob/main/docs/x-login.md). Instead, you can still export `X_BEARER_TOKEN` or
180
+ set `[x] bearer_token`; those win over a saved login. `status` and `doctor` show which one is used, never the token.
181
+
182
+ Reading bookmarks (`GET /2/users/{id}/bookmarks`) needs an **OAuth 2.0 user-context token with the `bookmark.read`
183
+ scope**, which typically needs a paid X API plan (check X's current pricing). An app-only bearer token does not work.
184
+ X does not return *when* you bookmarked a post, so the page shows when the post was published. If you cannot use the
185
+ API, `sync --from-file` accepts a saved response in the same JSON shape.
186
+
187
+ ## Paste this into Claude Code or Codex to install and configure it
188
+
189
+ ```text
190
+ Install and configure unhoard from this repository for me.
191
+ 1. Check Python >= 3.11, then run `pip install .` in a virtual environment and show me the path of
192
+ the `unhoard` command.
193
+ 2. Run `unhoard setup`. For "agent that writes the cards" use the CLI I am running right now.
194
+ 3. Run `unhoard demo` then `unhoard serve` and tell me the URL so I can look.
195
+ 4. Ask me whether I have X API access with a user token that has the bookmark.read scope. Do not ask me to
196
+ paste the token into this chat: tell me to `export X_BEARER_TOKEN=...` in my own shell or put it in a
197
+ 600-mode env file, and wait.
198
+ 5. When I confirm, run `unhoard sync`, then `unhoard enrich --limit 5`, then
199
+ `unhoard status`, and summarise the result.
200
+ 6. Run `unhoard doctor` and show me the table. Review the `[context]` lists that setup wrote with
201
+ me. Setup run without a terminal does not save agent memory folders or memory MCP servers: show me what
202
+ `unhoard context discover --dry-run` found and let me decide whether to run it with `--yes`.
203
+ Optionally ask for a short text file about my work and set [enrich] about_me_file to it.
204
+ 7. Offer to add a schedule from examples/schedule/. Never answer cards for me and never run
205
+ `unhoard decide`: only I decide.
206
+ ```
207
+
208
+ ## Commands
209
+
210
+ | Command | What it does |
211
+ | --- | --- |
212
+ | `try [--port N] [--no-open]` | The card page with fictional sample cards in a temporary folder that is removed on exit. Touches none of your own config or data; Go only shows the brief. |
213
+ | `setup [--yes] [--force] [--no-discover]` | Writes `config.toml` (mode 0600; asks about your X user id, agent and launch adapter on a terminal), looks for a local Omnigent and your agents, then runs `context discover`. |
214
+ | `login x [--client-id ID] [--port N] [--timeout SECONDS] [--no-browser] [--replace]` | Logs in to X in the browser (OAuth 2.0 + PKCE). `--timeout` is how long to wait for the browser (default 300 s). If a login is saved, it asks `[y/N]` before signing it out; with no terminal it refuses unless you pass `--replace`. `logout x` revokes and deletes the saved login. |
215
+ | `context discover [--yes] [--dry-run]` | Finds memory folders, notes, repos and memory servers and saves them to `[context]`. Without a terminal and without `--yes` it does not save agent memory folders or memory MCP servers. |
216
+ | `phone [--host NAME] [--port N] [--yes]` | Prints the steps to use the card page from your phone over Tailscale (or SSH). Reads this machine's Tailscale name; changes nothing unless `--yes` writes `[server] allowed_hosts`. |
217
+ | `agents [--yes]` | Lists the agents on your local Omnigent server and saves the one Go should start. |
218
+ | `doctor` | Shows what Go will do, then starts each enrichment pass with a trivial prompt and checks the tools it really has. |
219
+ | `sync [--full] [--refresh] [--max-items N] [--from-file JSON]` | Pulls new bookmarks. `--refresh` re-reads the pages and updates avatars, counts, media and links of bookmarks you already have (no decision or enrichment changes). `--from-file` ingests a saved X API response. |
220
+ | `enrich [--limit N] [--retry-errors]` | Has your agent write why-saved notes and tasks for unprocessed bookmarks. |
221
+ | `next [--more] [--json]` | Shows the best open card. `--more` ignores the daily limit. |
222
+ | `decide ID go\|later\|skip [--note ...]` | Answers a card. Go runs the launch adapter; a failed launch puts the card back. |
223
+ | `serve [--port N] [--no-open]` | The card page on 127.0.0.1. |
224
+ | `status` | Paths, counts, token present (yes/no) and its source, the agent, the configured and effective launch adapter, context and MCP tools in use, config problems. |
225
+ | `demo` | Adds fictional sample cards to your database. |
226
+
227
+ ## Configuration
228
+
229
+ `unhoard setup` writes a commented `config.toml`. Main settings:
230
+
231
+ - `[enrich] agent = "claude" | "codex"`: the program builds both agent commands when it runs, so fixes reach
232
+ you. `read_command` / `relate_command` (argv lists, prompt on stdin, JSON on stdout) remain as custom
233
+ overrides. Details and the Codex caveats are in [docs/agent-commands.md](https://github.com/vicennial/unhoard/blob/main/docs/agent-commands.md).
234
+ - `[context]`: what pass 2 may use to connect a post to your work. `setup` and
235
+ `unhoard context discover` fill it in for you (see below); `status` shows what is in use.
236
+ `[enrich] about_me_file` is optional.
237
+ - `[queue] daily_cards = 3`, `later_days = 3`.
238
+ - `[server] allowed_hosts = []`: extra host names the card page answers to (default none: loopback only). For phone access, see [From your phone](#from-your-phone). Host names only: no scheme, port, wildcard or IP address. Requests by such a name must come through an HTTPS proxy (`tailscale serve`), never with the server's own port, and carry Tailscale's identity header.
239
+ - `[server] require_tailscale_identity = true`: set `false` only behind another HTTPS proxy you trust; it turns off the identity-header check (see [docs/phone.md](docs/phone.md)).
240
+ - `[launch] adapter`: `auto` (default: Omnigent when it runs and an agent is chosen, else show the brief), `print` (show the brief), `command` (pipe it to a program you name, detached),
241
+ or `omnigent` (create a session: `POST {api}/v1/sessions` with your `agent_id`, in the `project` folder,
242
+ default "X Bookmarks"; optional `host_id`, `workspace`, `harness`, `model`).
243
+
244
+ Where things live: data in `$UNHOARD_HOME` (else the XDG data dir), config in
245
+ `$UNHOARD_CONFIG` (else `~/.config/unhoard/config.toml`). Paths are resolved to
246
+ absolute form once. **Agents and cron jobs may run with a different HOME than your shell, so export both
247
+ variables for them.** The schedule examples do this.
248
+
249
+ Run it on a schedule with [cron, a Claude Code `/loop`, or Hermes cron](https://github.com/vicennial/unhoard/tree/main/examples/schedule/).
250
+
251
+ ## What it reads by default
252
+
253
+ Only what `setup` / `context discover` lists in `[context]` after you accepted it (and you can edit it):
254
+
255
+ - agent memory folders (`~/.claude/projects/*/memory`, `~/.codex/memories`), note vaults (Obsidian, Logseq,
256
+ `~/notes`) and Git repositories you committed to in the last 30 days;
257
+ - `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` and each memory folder's `MEMORY.md`, inlined (capped);
258
+ - read-only search tools of memory MCP servers you already have, if they are known ones (`gbrain`,
259
+ `basic-memory`, the `memory` knowledge-graph server). Other servers are never used unless you list their
260
+ tools yourself. A cloud-hosted memory server receives the search text, which is your own provider.
261
+
262
+ Only the relate pass sees these, and it has no network. The raw post never reaches it (only pass 1's checked summary does), and the read pass
263
+ never sees any of it. Neither Claude pass loads your own Claude customization (CLAUDE.md, hooks, settings,
264
+ plugins, skills); `doctor` checks that. Codex supports folders but no MCP, and needs a working Linux sandbox for
265
+ its relate pass (`doctor` checks it).
266
+
267
+ The Claude presets use `--no-chrome`; they are tested with Claude Code 2.1.303 (an older version may lack the flag).
268
+
269
+ ## Privacy and safety
270
+
271
+ - Everything is local: a SQLite file (mode 600) on your machine and a server on `127.0.0.1`.
272
+ - The **only** place bookmark text leaves your machine is the agent command *you* configure, which may
273
+ send it to a model provider. The X token is removed from that command's environment, and the command
274
+ runs in a scratch directory, not in your projects.
275
+ - **No agent gets all three of untrusted text, your private data and a way to send data out.**
276
+ Enrichment runs in two passes. Pass 1 reads the post (and the web) but never sees your files or notes.
277
+ Pass 2 sees your context but only a checked summary of the post, and has local read tools and no
278
+ network. Built-in presets are constructed to keep that rule and start without your Claude settings, CLAUDE.md,
279
+ hooks or allow rules, and with Claude in Chrome and claude.ai connectors off. The program refuses to start a
280
+ custom Claude command that breaks the rule or uses a flag it does not know (only the per-pass
281
+ `allow_private_in_read` / `allow_network_in_relate` overrides lift that), and warns when it cannot read a
282
+ custom command at all. `unhoard doctor` verifies the real behaviour. Details in
283
+ [docs/agent-commands.md](https://github.com/vicennial/unhoard/blob/main/docs/agent-commands.md).
284
+ - Profile pictures, photos and video are fetched by your browser from X's servers (`pbs.twimg.com`,
285
+ `video.twimg.com`); the page's content-security policy allows nothing else. Set `[server] show_media = false`
286
+ to turn all of that off.
287
+ - **Bookmark text is untrusted input.** It is fenced as data in the enrichment prompt, the answer is
288
+ validated field by field before it is stored, the card page renders text without HTML, and the launch
289
+ brief repeats the warning and fences the model-written task as derived data. Nothing starts until you press Go.
290
+ The card page marks every URL the relate agent wrote into a task, so you can look at it before you press Go.
291
+ - The card server checks the Host header (DNS rebinding), rejects foreign Origins, and requires a custom
292
+ header on every state-changing request.
293
+
294
+ ## Layout
295
+
296
+ ```
297
+ src/unhoard/
298
+ config.py store.py paths, config.toml, SQLite schema
299
+ sync.py X API v2 bookmarks -> SQLite
300
+ enrich.py two-pass prompts, context pack, command plan, strict validation
301
+ postdata.py avatar, counts, media and readable text from the X API objects
302
+ agents.py mcp.py agent presets and the custom-command guard; memory-MCP registry and config file
303
+ procs.py run a helper in its own process group (killed on timeout)
304
+ discover.py tomledit.py context discovery and the atomic config edit that saves it
305
+ doctor.py the doctor checks
306
+ queue.py ranking, daily limit, Go / Later / Skip, launched sessions
307
+ launch/ auto | omnigent | command | print adapters, the target resolver and the brief template
308
+ netsafe.py HTTP openers that refuse redirects (and ignore proxies for loopback probes)
309
+ server.py web/ the card page (stdlib http.server + static files)
310
+ cli.py the unhoard command
311
+ src/unhoard_ext/ Omnigent extension scaffold (manifest + a page that explains what is missing)
312
+ examples/schedule/ cron, Claude Code /loop, Hermes cron
313
+ ```
314
+
315
+ ## Development
316
+
317
+ ```bash
318
+ python3 -m unittest discover -s tests # no network, no real agent calls
319
+ UNHOARD_LIVE=1 python3 -m unittest discover -s tests # also runs a live smoke test against claude/codex
320
+ ```
321
+
322
+ The extension-manifest test is skipped unless `omnigent` is importable in the same environment.
323
+
324
+ ## Known limits (v0.1)
325
+
326
+ - The Omnigent extension tab is a scaffold; see "Coming" above.
327
+ - Re-enriching a bookmark replaces its undecided proposals; decided ones are kept.
328
+ - Only bookmarks are read: no folders, likes or lists.