The OpenCode TUI renders tool output through a literal <text> node
(packages/tui/.../session/index.tsx, GenericTool), so a GFM table
becomes a wall of pipes. The web/desktop client passes the same output
through the markdown renderer and draws a proper grid, so a GFM table
is the most compact form there.
The previous fix emitted a markdown table and assumed the TUI would
render it. It does not. The fix: ship both. The ASCII table is space-
padded, word-wraps long cells, and stays aligned even when the TUI
shows only its collapsed 3-line preview; the markdown table follows
and is what the web/desktop client renders with a proper grid.
- New rowsToAsciiTable: cellToAscii, truncateForCell, wrapCell,
visibleWidth, padRight. NULL -> <null>, BLOB -> <BLOB nB>, Date ->
ISO, BigInt -> string. Cell width capped at 40 chars by default
(configurable). Single words longer than the cap are hard-wrapped.
- sqlite_query's format=table path now emits the ASCII table first
and the markdown table second, with a blank line between.
- README explains why the duplication and how to read both.
End-to-end verified via Code Mode (no model in the loop) after
opencode service restart picked up the new transform.
OpenCode's tool content schema (Tool.Content = TextContent | FileContent)
gives a server plugin no metadata channel — the TUI's only view of tool
output is the content string. So: format the result as a Markdown
table, which the TUI's built-in markdown renderer already displays as
a real table.
- sqlite_query gains format = 'table' (default) | 'json' | 'both'.
'table' is the new default and renders a header line, a separator
row, and one row per record with pipes/newlines/backslashes escaped.
'json' keeps the old behaviour for callers that need raw rows.
- Every tool that runs SQL echoes 'SQL: <stmt> params: [...]' as the
first line of its output, so the user sees what was executed
including the bound values.
- Every tool that runs a statement pushes context.progress() at start
and end, which surfaces in the TUI status bar.
- cellToMarkdown caps each cell at 80 chars to keep wide BLOBs from
blowing up the TUI column width.
End-to-end verified via Code Mode (no model in the loop):
- format=table renders correctly with edge cases (| in text, \n in
text, \ in text)
- format=both appends the JSON view below the table
- exec echoes the bound params as a JSON array
- write+read round-trip persists (UPDATE → SELECT sees the new value)
User asked what happens if the window is closed mid-task. Added a
'What happens if the process dies mid-task' section that tabulates
the four exit paths (TUI close, SIGTERM, SIGKILL, power loss) and
empirically confirms that anything past the last fsync'd commit is
on disk. The only ways to lose data are :memory:, an explicit
synchronous=OFF, or a network filesystem. Also corrects the earlier
'leak fd' framing — a dead process leaks no fds, the OS reclaims
them.
User asked what happens if the connection is never closed and whether
concurrent calls are safe. Adds a Lifecycle and concurrency section
to README.md covering:
- not closing only leaks fds; data is durable on commit
- 3-tier concurrency model: same service / multi-CLI same service /
multi-process, with the actual busy_timeout default (5s) and what
to do if a longer wait is needed (run PRAGMA busy_timeout via
sqlite_exec)
- :memory: semantics (gone on close, even within one process)
- NFS/SMB caveat
No code changes; current defaults (busy_timeout 5s, synchronous
FULL, locking normal) are sensible and the existing test sweep
already verified end-to-end.
Adds a single-file V2 plugin that exposes eight tools under the
`sqlite` namespace (open, close, list, query, exec, script, tables,
schema) so a model can read and write SQLite databases from a session.
- OpenCode discovers it automatically from ~/.config/opencode/plugins/.
- New runtime dependency: better-sqlite3 (installed in this repo's
node_modules so the host's module resolution picks it up).
- README documents install, copy-to-another-machine, options, and
troubleshooting; SETUP.md is a copy-paste checklist.
- Schema/options were trimmed down while authoring: the initial draft
registered a no-op SQLite function on every open as a 'future hook';
removed, it was dead code.
Install the three-level file-based agent memory (kurbezz/memory):
skills land in ~/.agents/skills/agent-memory via 'npx skills add',
and plugins/agent-memory.js is the installer-created symlink into it.
Pure markdown store under ~/.agent-memory, no vector DB and no API key.
Asked for a domestic-only default, so every general-web index SearXNG ships
for China was added to keep_only, the container was restarted, and each was
measured over 5 runs per query. None of them is usable. The default stays
bing,yandex.
engine en hits zh hits outcome
360search 5/5 5/5 reliable, 0% relevant
quark 0/5 0/5 rate-limited dead after two good responses
baidu - 0 CAPTCHA, 1h IP suspension
sogou - 0 upstream AttributeError
baidu kaifa - 0/10 code index: "opencode v2" matches OpenCV2
bilibili - - video titles, not a web index
chinaso news - - falls through to the default engine set
quark was the one that looked like the fix. On its first two requests it
returned the exact article being asked about as result #1 on both the Chinese
and the English query -- better than anything else measured -- and then answered
0 results on 5 consecutive runs of each. Had it not been run 5 times, it would
have looked like the obvious upgrade.
baidu kaifa is worth remembering as a category error: it is a code search
index, so it matches "opencode v2" against OpenCV2 and returns CSDN blog posts
at 0% relevance. It is reachable and does not CAPTCHA, which makes it look
healthy in /config while being useless.
The foreign engines that were reachable (brave, duckduckgo, google, qwant,
wikipedia) were added at the same time and all returned 0, consistent with the
network timeouts already recorded in settings.yml. mojeek and startpage are not
present in this SearXNG release at all, so the earlier note calling them
"unreachable" was imprecise.
settings.yml was restored from a backup and diffed byte-identical before this
commit; the probe engines never left the instance.
cn.bing.com is the only dependable endpoint and is a China-hosted Bing, so bing
counts as the domestic option. Its one real limitation stands: it ignores the
site: operator, so site-restricted search is unavailable through this instance.
Also fixes a duplicated `bing` row left in the README engine table by the
previous commit.
The default was chosen by a single Chinese test query and generalized from
it. Measuring both a Chinese and an English technical query shows it was
inverted on both counts. Results mentioning "opencode", out of returned:
zh "OpenCode V2 和 V1 区别" en "opencode v2 migrate from v1 plugin api"
bing 10/10 missed the doc entirely
360search 0/5 4, unrelated (sohu/toutiao)
yandex 14/15 15, migration doc ranked #1
naver 0/9 15, migration doc ranked #1
yandex had been excluded on the claim that it "has almost no Chinese index".
Measured, it has the best Chinese relevance of any engine here, and on the
English query the V1->V2 migration doc is its first result -- the exact
document that the old default could not find at all.
360search had been the default. It is the worst engine on both query types.
It splits the query on "V1"/"V2" and answers with Android and CANN
version-difference articles, which produced the 10-of-15 noise in the first
round of testing.
naver is left out rather than added: strongest on English, 0/9 on Chinese, so
including it would trade one kind of noise for another. It is documented as
worth passing explicitly for English lookups.
The comment above the constant now carries the measurement table, so the
default can be re-chosen from data instead of re-derived from a hunch.
Not changed, and worth knowing: cn.bing.com ignores the site: operator.
engines=bing&q=site:opencode.ai+v2+migration returns 0 results from
opencode.ai, while the same query without site: returns 3. Site-restricted
search is not available through this instance.
Unreproduced: one run of the Chinese query returned entirely unrelated
results from bing (Doubao, Zhipin, Zhihu) while four other runs with the same
parameters returned 10/10 relevant. No parameter difference was found between
the good and bad runs. Treated as transient upstream behaviour, not a plugin
bug, and not documented as a rule.
README engine table updated to the measured numbers.
Two docs next to the plugin: README.md for the tool, SEARXNG-SETUP.md for the
Docker deployment behind it. Both are written from measurements against the
running instance, not from a guide.
The setup doc exists because the deployment decisions were only recorded as
comments inside settings.yml, which means they are unfindable unless you
already know to look there. It also records the parts that are not guessable:
why the port is 39217, why limiter is off, why the UI locale is zh-Hans-CN
rather than zh-CN, and which engine is broken how.
Corrections found while writing it:
- The rebuild steps pointed at searxng/searxng-docker, which is superseded. That
repository now holds only a deprecation notice and its docker-compose.yml raw
URL 404s. The templates moved to searxng/master/container/. Verified both
local files are byte-identical to the current templates, so the deployment
was always the current one -- only the documented URL was wrong.
- The plugin README claimed the entrypoint comes from package.json. This
directory has no package.json; the entrypoint falls back to index.ts. Restarted
the service with both .md files in place to confirm they do not affect
resolution.
Documents the two SearXNG behaviours that fail silently rather than loudly,
both verified by reading the engine label on each result:
- An unknown engine name is ignored and the default engine set is searched
instead, with no error. engines=mojeek returns 10 bing results and looks like
a success, as does a typo. The only reliable signal is whether the requested
engine appears in results[].engine.
- /config is not a way to check this. It reported disabled: false for every
engine while baidu and sogou were in fact being dropped, because it reflects
upstream defaults and not the local disabled: true overrides.
Also records that ctx.options is always {} for a directory-discovered plugin,
so the four options in PluginConfig cannot be supplied without moving the
plugin out of plugins/ and registering it in opencode.jsonc. Verified by
writing JSON.stringify(ctx.options) to a file from inside setup().
No secret_key value is in either file; settings.yml stays outside this
repository under ~/Docker.
Repository sits at the OpenCode config root rather than inside skills/ or
plugins/. OpenCode scans exactly those two subdirectories, and a .git
directory inside either one is picked up as a plugin and fails to load:
loading plugin id=.../plugins/.git entrypoint=.../plugins/.git/index
failed to load plugin ... AggregateError: 2 errors building .../plugins/.git/index
The watcher.ignore config option does not help: it only suppresses filesystem
update events, not plugin discovery, and the "-" disable prefix only applies to
plugins listed in opencode.json(c), not auto-discovered ones.
.gitignore admits only skills/ and plugins/ (plus itself). Personal config
(opencode.jsonc, cli.json, service.json) and node_modules stay local.
skills/opencode-plugins V2-only plugin authoring reference and 7 templates,
transcribed from the V2 docs with the URL on every file
plugins/searxng-search searxng_search tool over a self-hosted SearXNG JSON API