DeepSeek Harness Remote
DeepSeek Harness Remote 可将手机或另一台电脑与你运行 DeepSeek Harness 的电脑安全连接,随时随地继续你的工作,方便随时查看进度并继续工作。
AI 翻译
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Annals for AI agents. An agent publishes a document (markdown, html, an image, a video, a PDF, or a whole folder of renders) with one command and gets back a link; the owner opens their annals on a phone and finds everything their agents left for them, newest first, searchable, with a recycle bin.
pip install annals
annals publish report.md --session my-agent
# https://apps.example.com/annals/d/20261003-203301-quarterly-report-3f9a
Beside what agents publish, the page can list other folders read in place, such as the sessions' ledgers (~/.local/share/crowsnest/ledger): one search over every word of every item, filters by source, kind, session, date and tag, and tags you add from the page, kept apart from the files.
Documents are plain files in a directory. The directory can be on this machine or on another one over ssh, and the page that serves it reads it directly, so publishing never redeploys anything. No database, no build step, no API token.
Load the shipped skill, annals-publish, and you have the whole protocol: one command, one link, put the link in your reply.
annals publish report.md # one document, prints its link
annals publish a.md b.md c.html --group "Review" # one link for a set (the group link prints first)
annals publish d.md --add-to <group-id> # add to that set later (same link)
annals publish ./renders # a folder: a gallery with inline images, video, audio
annals publish ./site_dir # an html page with its assets, as one document
annals publish clip.mp4 # media plays inline (streamed, seekable)
some-command | annals publish - --title "Log" # from stdin
annals publish guide.html --replace <link-or-id> # a new version of a document: same link
annals publish page.html --artifact <claude.ai artifact link> # plus a button to its interactive version
annals ls "search words" # what is there, newest first
annals trash <id> / annals restore <id> # the recycle bin
What each kind looks like on the page:
| Published | Shown as |
|---|---|
a .md file | rendered markdown, with a toggle to the source and a copy button; its relative images and links resolve to its own files |
an .html file, or a folder with index.html | the page itself, in a sandboxed frame |
| an image, video, audio file or PDF | inline: image, player (byte-range streaming, so video seeks), PDF viewer |
| a folder with one page in it | that page, with the other files below it |
| any other folder | a gallery: image thumbnails (a page at a time), players, a file list; every file opens on its own with prev/next |
| anything else | a download |
A folder skips hidden files and caches (.*, __pycache__, *.pyc); --all-files keeps them.
--artifact)annals publish page.html --artifact https://claude.ai/... stores the link of a Claude Artifact holding the same page in the document's meta (artifact), and the document's page shows an "Open the interactive version" button next to the frame. Comments, per-viewer and shared state and sharing then live on the artifact while annals stays the single in-tray. Only plain https links to claude.ai are accepted (annals.store.ARTIFACT_HOSTS; no port, credentials or odd characters), --artifact takes one path, and a --replace without --artifact keeps the current link.
--replace)annals publish <path> --replace <link-or-id> publishes a new version of an existing document: the id and the link stay, the page shows the new version marked "updated" and lists the earlier ones, each readable at /d/<id>@<n>. The previous files are kept under the document's .versions/<n>/; nothing is deleted. It takes one path (a file, a folder, stdin), and a replaced group member stays in its group.
A replacement is written next to the document as a pending directory (docs/<id>.r<stamp>-<hex>/), never over it, and applied by whoever reads the store (the server, or the next publish): a few renames, the first of which claims the document, so concurrent readers never apply one twice or out of order. That is what lets a replace travel through an outbox, a directory another machine pulls and empties: mark it with annals configure --outbox (or ANNALS_OUTBOX=1), and replacements published there are left for the server to apply when they land. HTML pages without a doctype are served with <!doctype html> (plus a charset and viewport when missing), so they render in standards mode.
With pip install 'annals[mcp]', the same operations are an MCP server: python -m annals.mcp.
annals configure --target tw:/root/.local/share/annals --base-url https://apps.example.com/annals
That writes ~/.config/annals/config.toml. target is a directory, or host:/path for a directory on another machine reached by ssh host (keys, no prompt; host can be an alias from ~/.ssh/config). ANNALS_TARGET and ANNALS_BASE_URL override it per shell. With no configuration, documents go to ~/.local/share/annals and the link points at a local annals serve.
--to)A config file can name more places: annals configure --to team --target host:/path --base-url https://team.example.com/annals saves a [targets.team] table and leaves the default alone. Then annals publish r.md --to team (also ls, show, trash, restore, group, groups) works on that place only, and annals copy <link-or-id> --to team [--source other] copies documents or whole groups there, keeping their ids. Nothing about the place is in the code: a name, a target and a base url.
Inside an app platform (the usual case). Mount the API under the platform's prefix and let the platform's login gate it:
# server.py of the host app; the platform serves frontend/ at /annals/ and this at /api/annals
from annals.api import mk_api
app = mk_api(data_dir="/root/.local/share/annals")
and write the page shell once as the app's frontend:
annals page-shell --api /api/annals --base /annals > frontend/index.html
The shell only names those two paths; the page's script and style load from the API, so upgrading the package upgrades the page. Gallery thumbnails are made with Pillow when it is installed (cached under the data root's cache/), and fall back to the original image when it is not.
Standalone, for a machine with no platform:
pip install 'annals[server]'
ANNALS_DATA_DIR=~/.local/share/annals annals serve --host 127.0.0.1 --port 8765
The page is at http://127.0.0.1:8765/annals/, its API under /annals/api/. Auth is chosen by environment variables:
| Situation | Set | Effect |
|---|---|---|
| Behind an existing login that exposes a who-am-I endpoint | ANNALS_WHOAMI_URL, [email protected], optionally ANNALS_LOGIN_URL=/auth/login | the browser's cookies are forwarded to that endpoint and the listed emails are allowed; anyone else is sent to the login page |
| No platform login | ANNALS_BASIC_USER, ANNALS_BASIC_PASSWORD | HTTP Basic |
| Localhost only | nothing | open |
A document's page can offer a Listen button: a few minutes of speech about the document, played with its script in sync. It is off unless the host turns it on, because making one spends money.
app = mk_api(overviews=True) # the button, and POST /docs/{id}/overview
A worker on the same machine fulfils the requests. It runs a command you choose, which takes a text file and writes one HTML page, and publishes that page as a document of its own:
export ANNALS_OVERVIEW_COMMAND='hailing overview {source} --minutes {minutes} --no-publish --player-to {out}'
annals listen-work # fulfils what is queued, one at a time
annals listen <doc-id> --minutes 3 # asks for one from the command line, as the button does
Run annals listen-work from a timer, not from a watcher on the folder: finished request files stay there, so a watcher would keep waking it. A current overview is not made again unless forced, and at most 20 are started in any 24 hours (ANNALS_OVERVIEW_DAILY_LIMIT). annals.overview has the details.
The annals is private; a share link lets someone without an account read ONE document (or one group) and nothing else. From the page, the Share button on a document lists its files and the annals pages it links to, so you choose what goes; from a shell:
annals share <id-or-link> --password - # 7 days by default; password from stdin
annals share <id> --expires 2026-11-30 # or 12h, 30d, an ISO time, `never`
annals share <id> --include img/chart.png,img/map.png # only these files (the page always goes)
annals shares # live links (--all: revoked and expired too)
annals unshare <link-or-token> # off at once
The link is <share_base_url>/<token>/: 128 random bits, noindex, no-referrer. Every request is checked against the share's record (shares/<token>.json under the data root), so a file you did not choose is a 404 even beside the shared page, a revoke or an expiry takes effect on the next request, and references to anything outside the share (an image you left out, a link to another annals page or to anything else on your annals' host, an annals id in the text) are removed from every text file before it is sent (markdown, html, plain text, svg). A linked page you tick goes with the files it shows, not every file beside it. The password is a gate on the link, not an account: at least 8 characters, stored hashed (stdlib scrypt), with attempts limited per client and per link, and a correct one earns a cookie scoped to that link. Links are read-only and last 7 days unless you say otherwise.
Where the links are served:
annals serve): at /annals/shared/<token>/, outside the login, with nothing to configure.# server.py of a second, PUBLIC app (the owner's app keeps its login)
from annals.share_api import mk_share_api
app = mk_share_api(data_dir="/root/.local/share/annals")
then set ANNALS_SHARE_BASE_URL (or annals configure --share-base-url ..., or mk_api(share_base_url=...) for the owner's page) to that app's public URL. Until it is set, shares are made but carry no link. mk_share_api takes rate_limiter= and link_failure_limiter= (anything with allow(key) -> bool, such as heed's Redis limiter; the defaults count per process), trusted_proxies= (whose X-Forwarded-For names the client; loopback by default) and private_hosts=; the password hasher is a keyword of annals.share.Shares (argon2, or anything with hash/verify).
Every markdown page has an Actions menu (Share, Copy, Download, View) with two downloads made by the server: Markdown (.md) and a titled PDF with the images inside (audio, video and other files appear as a link to the resource). Both are named after the title (quarterly-report-q3.pdf) and are served under the same rules as the page: the owner's login, or a share link's scope (a recipient gets the filtered text and only the files the link includes). The PDF needs pip install "annals[pdf]" (WeasyPrint, which uses Pango; annals[server] includes it); without it the PDF action answers with that hint. Pages (/d/<id>) are drawn in the browser by app.js, so a page that gains an action gains it for every existing document.
A group's page has a Combine button: it makes one Markdown file from every .md in the group, folder documents included, and offers Copy Markdown, Download .md and Open as text, with the text shown below for selecting by hand where a phone refuses to copy. The file has the group's title, a table of contents, and one ## section per file named after its path (Report/01-intro.md); paths that start with a number come first, in numeric order, the rest in the group's order; each file's headings go one level down and hard-wrapped lines are joined. Footnotes are renamed per file so they do not collide, and a code block a file leaves open is closed at the end of its section. Relative links and images keep their paths, so they do not resolve inside the combined file. It is made on each request and never stored. A share link to a group has the same button, built from the share's filtered text and only the files it includes.
annals aggregate <group-id-or-link> > all.md # the same file, from a shell
annals aggregate <id> --order given --demote 0 --no-toc --keep-wraps
from annals.aggregate import aggregate_markdown
aggregate_markdown({"notes.md": "# Notes", "01-intro.md": "Hello"}, title="Bundle", demote=1)
aggregate_markdown takes (path, text) pairs and knows nothing of annals: order= (numbered_first by default, None for the order given, or any paths -> paths callable), demote=, unwrap=, toc=. The API is GET /groups/<gid>/aggregate (?download=1 to save it, &demote=), and GET /<token>/aggregate on the share server.
To take a group into a notebook (NotebookLM, now Gemini Notebook) with each document as its own source, open the group and tap Notebook. Tick the documents (all are ticked), choose how long the links work (1 day by default), and tap Make links. You get one link per page; tap Copy N links, then in the notebook choose Add source → Website and paste: the notebook makes one source of each link, named after the document. The same panel's Combine these makes one Markdown file of only the ticked documents.
The links are an ordinary share of just those documents (no password, since the notebook cannot type one): unguessable, filtered like every shared text, and revocable with Turn these links off or under Shares. The notebook keeps its own copy once a link is added, so turning the links off afterwards does not remove the sources. Each link is a plain web page with no script (<share link>/source/<doc id>/<file>); markdown is rendered on the server, images, audio and PDFs get no link (a notebook takes those only as uploads). Why links and not a zip or a download: misc/docs/notebook-sources.md.
The API: GET /shares/<token>/sources (the owner's) or GET <share link>/sources (the recipient's) lists {title, url} per page; GET /groups/<gid>/aggregate?doc=<id>&doc=<id> combines only those documents.
Name extra folders when you mount the API, and they appear beside the published documents, each in its own section (/annals/s/<name>) and in /annals/all:
app = mk_api(data_dir="~/.local/share/annals", sources={"ledgers": "~/.local/share/crowsnest/ledger"})
or, with no code, ANNALS_SOURCES="ledgers=~/.local/share/crowsnest/ledger" (several: comma-separated). A folder is read in place and never written: its files are listed newest first with their age, markdown renders, html is framed, the rest shows by its kind, and a sub-folder's files count as written by the sub-folder's name. Hidden files and caches are skipped.
On every list:
tags/tags.json), so a ledger is never touched; on a published document they replace the tags the publisher gave.The API behind it: GET /items?q=&source=&trash= → {data, total}, GET /items/<source>:<id>, PATCH /items/<source>:<id> with {"tags": [...]}, GET /tags, GET /sources, and GET /files/<source>/<path> for a folder's raw files. The page reaches items through @zodal/store-http's provider (vendored; rebuild with misc/vendor/build.sh).
from annals import DocStore, publish
meta = DocStore("~/.local/share/annals").publish("report.md", tags=["q3"])
publish(["report.md"], tags="q3")["url"] # same, through the configured target
annals.api.mk_api(...) is the mountable API; annals.api.mk_app(data_dir=..., authorizer=...) the standalone app. Both take sources= (name → folder path, or any annals.sources.Source) and tags= (any MutableMapping from item key to tag list; default JsonTagStore). annals.catalog.Catalog is the facade they stand on: every source's rows as one list, searched, with the owner's tags.
Flat store with tags and groups, not a hierarchy: agents from many projects do not share one, and every placement decision costs tokens. A document is docs/<id>/meta.json plus its files; the bin is trash/<id>/; a group is groups/<gid>.json. Ids are YYYYMMDD-HHMMSS-<slug>-<4 hex>, so a listing is already in time order and a URL says what it points to. The transport is a seam (annals.target): local directory, ssh, and later an http ingest endpoint. More in misc/docs/design.md.
gh skill install thorwhalen/annals annals-publish
The skill also ships inside the package at annals/data/skills/annals-publish/.