witbitz-code 1.14.0__tar.gz → 1.15.2__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.
- {witbitz_code-1.14.0/src/witbitz_code.egg-info → witbitz_code-1.15.2}/PKG-INFO +1 -1
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/pyproject.toml +1 -1
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/connector.py +7 -4
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs +32 -4
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs +5 -5
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-browser.js +2 -1
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/outputs.py +5 -3
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/ui.py +3 -2
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/ui_static/app.css +1 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/ui_static/app.js +8 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/ui_static/index.html +5 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2/src/witbitz_code.egg-info}/PKG-INFO +1 -1
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_outputs.py +30 -1
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_ui.py +1 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/LICENSE +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/README.md +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/setup.cfg +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/app.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/browse.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/browsetree.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/cli.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/confidential.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/confidential_models.json +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/dirs.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/files.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/icons/witbitz-code.ico +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/images.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/mkdir.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-browse-publisher.mjs +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/pair.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/ports.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/remind.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/seccheck.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/selfupdate.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/tools_catalog.json +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/toolsetup.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/uitools.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/uninstall.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/winagent.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/winbg.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/winconsole.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/wizard.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/wslstart.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code.egg-info/requires.txt +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_asks.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_attachments.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_auto.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_browse.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_browsetree.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_cli.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_codetools.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_confidential.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_connector_e2e.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_dirs.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_files.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_folders.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_images.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_pair_flows.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_plugins.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_policy.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_ports.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_ports_win.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_registry.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_remind.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_seccheck.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_selfupdate.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_setup.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_toolsetup.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_ui_page.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_uitools.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_uninstall.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_winagent.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_winbg.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_winconsole.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_windows_files.py +0 -0
- {witbitz_code-1.14.0 → witbitz_code-1.15.2}/tests/test_wslstart.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "witbitz-code"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.15.2"
|
|
8
8
|
description = "Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -164,9 +164,11 @@ class PairingServer:
|
|
|
164
164
|
notes_plugin_path: Path | None = None, payers: SessionPayers | None = None,
|
|
165
165
|
model_url: str = "", remind_times: dict | None = None,
|
|
166
166
|
browse_probe: Callable[[], dict | None] | None = None,
|
|
167
|
-
browse_private: Callable[[bool], Awaitable[bool]] | None = None) -> None:
|
|
167
|
+
browse_private: Callable[[bool], Awaitable[bool]] | None = None, screens_root: Path | None = None) -> None:
|
|
168
168
|
self.pairing = pairing
|
|
169
169
|
self._browse_private = browse_private # private browsing on/off (browse.py LocalBrowser.set_private)
|
|
170
|
+
# the agent's browser_screenshot writes <home>/.witbitz/screens/<session> (browse-cdp.mjs), served beside the session's folder
|
|
171
|
+
self._screens_root = Path(screens_root) if screens_root else Path.home() / ".witbitz" / "screens"
|
|
170
172
|
# THE AGENT'S BROWSER ON THIS COMPUTER (browse.py): its /status rides the hello; a change is announced within 2 s
|
|
171
173
|
self._browse_probe = browse_probe
|
|
172
174
|
self._browse_said = "" # the last browse report the page was told, as JSON
|
|
@@ -413,7 +415,8 @@ class PairingServer:
|
|
|
413
415
|
await reply(404, {"error": "no such session on this computer"})
|
|
414
416
|
return
|
|
415
417
|
stat_only = q.get("stat") == "1"
|
|
416
|
-
st, b = serve_output(directory=session["directory"], path=q.get("path"), stat=stat_only
|
|
418
|
+
st, b = serve_output(directory=session["directory"], path=q.get("path"), stat=stat_only,
|
|
419
|
+
shots=(self._screens_root / sid).as_posix())
|
|
417
420
|
self._log_output({"at": int(time.time() * 1000), "session": sid, "digest": hashlib.sha256(q.get("path", "").encode()).hexdigest(),
|
|
418
421
|
"stat": stat_only, "st": st})
|
|
419
422
|
await reply(st, b)
|
|
@@ -1007,7 +1010,7 @@ async def start_connector(pairings: list[dict], *, flush_ms: float = 120, log: C
|
|
|
1007
1010
|
notes_plugin_path: Path | None = None, payers: SessionPayers | None = None,
|
|
1008
1011
|
model_url: str = "", remind_times: dict | None = None,
|
|
1009
1012
|
browse_probe: Callable[[], dict | None] | None = None,
|
|
1010
|
-
browse_private: Callable[[bool], Awaitable[bool]] | None = None) -> Connector:
|
|
1013
|
+
browse_private: Callable[[bool], Awaitable[bool]] | None = None, screens_root: Path | None = None) -> Connector:
|
|
1011
1014
|
"""Serve the given pairings until stop()/aclose(). Options exist for tests: flush_ms, log, client, connect,
|
|
1012
1015
|
request_timeout_ms, max_senders, max_response_bytes, auto_dir, auto_poll_ms."""
|
|
1013
1016
|
client = client or local_client()
|
|
@@ -1023,7 +1026,7 @@ async def start_connector(pairings: list[dict], *, flush_ms: float = 120, log: C
|
|
|
1023
1026
|
max_senders=max_senders, max_response_bytes=max_response_bytes, auto_dir=auto_dir, auto_poll_ms=auto_poll_ms,
|
|
1024
1027
|
attach_root=attach_root, attach_max_file_bytes=attach_max_file_bytes, asks_reconcile_ms=asks_reconcile_ms,
|
|
1025
1028
|
notes_root=notes_root, notes_plugin_path=notes_plugin_path, payers=payers, model_url=model_url, remind_times=remind_times,
|
|
1026
|
-
browse_probe=browse_probe, browse_private=browse_private)
|
|
1029
|
+
browse_probe=browse_probe, browse_private=browse_private, screens_root=screens_root)
|
|
1027
1030
|
await s.start()
|
|
1028
1031
|
servers.append(s)
|
|
1029
1032
|
return Connector(servers, client, payers)
|
{witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs
RENAMED
|
@@ -101,7 +101,24 @@ async function snapshot (c, limit = 120) {
|
|
|
101
101
|
// The refs are only meaningful next to the snapshot that made them, so they travel with it rather than living in
|
|
102
102
|
// a cache that a page navigation would quietly invalidate.
|
|
103
103
|
const REFS_FILE = (process.env.TMPDIR || '/tmp') + '/wb-browse-refs.json'
|
|
104
|
-
const { writeFileSync, readFileSync } = await import('node:fs')
|
|
104
|
+
const { writeFileSync, readFileSync, mkdirSync, readdirSync, statSync, rmSync } = await import('node:fs')
|
|
105
|
+
const { join } = await import('node:path')
|
|
106
|
+
const { homedir } = await import('node:os')
|
|
107
|
+
|
|
108
|
+
// THE SCREENSHOTS' FOLDER: <home>/.witbitz/screens/<session> (spaces/public/codeOutputs.js SHOTS_DIR) — the agent's home, as
|
|
109
|
+
// the connector reads it (on a cloud computer /rw/home, on a laptop the person's own). WB_BROWSE_SCREENS moves the root.
|
|
110
|
+
const SHOTS_ROOT = process.env.WB_BROWSE_SCREENS || join(homedir(), '.witbitz', 'screens')
|
|
111
|
+
const shotsDir = (sid) => join(SHOTS_ROOT, sid)
|
|
112
|
+
const SHOT_DAYS = 30
|
|
113
|
+
/** A session's pictures go 30 days after its last one — attachments' rule (tools/code-attachments.mjs). */
|
|
114
|
+
function pruneShots () {
|
|
115
|
+
try {
|
|
116
|
+
for (const n of readdirSync(SHOTS_ROOT)) {
|
|
117
|
+
const d = join(SHOTS_ROOT, n)
|
|
118
|
+
try { if (Date.now() - statSync(d).mtimeMs > SHOT_DAYS * 86400000) rmSync(d, { recursive: true, force: true }) } catch { /* */ }
|
|
119
|
+
}
|
|
120
|
+
} catch { /* none yet */ }
|
|
121
|
+
}
|
|
105
122
|
|
|
106
123
|
// ★ A MISSING refs file is the SAME situation as a stale ref: the agent has not taken a snapshot yet. Letting
|
|
107
124
|
// readFileSync throw turned that into "ENOENT: no such file or directory", which tells a model nothing it can act
|
|
@@ -226,11 +243,22 @@ try {
|
|
|
226
243
|
await new Promise((r) => setTimeout(r, 400))
|
|
227
244
|
} else if (op === 'screenshot') {
|
|
228
245
|
const { data } = await c.send('Page.captureScreenshot', { format: 'jpeg', quality: 70 })
|
|
229
|
-
|
|
230
|
-
|
|
246
|
+
// ★ INTO WITBITZ'S OWN FOLDER, ONE PER SESSION (the owner, 2026-10-25: "how can I get a screen snapshot in the chat"):
|
|
247
|
+
// <home>/.witbitz/screens/<session>/ — the connector serves that folder for that session as well as the session's own
|
|
248
|
+
// (tools/code-outputs.mjs), so the picture shows under the agent's answer, and nothing lands in the project. In a temp
|
|
249
|
+
// folder the page could never be shown it: the connector serves nothing outside a session's folders.
|
|
250
|
+
let path = args.path
|
|
251
|
+
if (!path) {
|
|
252
|
+
const sid = /^[A-Za-z0-9_-]{1,128}$/.test(String(args.session || '')) ? args.session : 'no-session'
|
|
253
|
+
const dir = shotsDir(sid)
|
|
254
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
255
|
+
pruneShots()
|
|
256
|
+
path = join(dir, 'screen-' + new Date().toISOString().replace(/[:.]/g, '-') + '.jpg')
|
|
257
|
+
}
|
|
258
|
+
writeFileSync(path, Buffer.from(data, 'base64'), { mode: 0o600 })
|
|
231
259
|
const { lines } = await snapshot(c, 0).catch(() => ({ lines: [] }))
|
|
232
260
|
c.close()
|
|
233
|
-
out({ ok: true, path, note: 'a picture for a person to look at; act from a snapshot, not from this' })
|
|
261
|
+
out({ ok: true, path, shot: true, note: 'a picture for a person to look at; act from a snapshot, not from this' })
|
|
234
262
|
} else if (op !== 'snapshot') {
|
|
235
263
|
out({ error: 'unknown op: ' + op })
|
|
236
264
|
}
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
// tools/browse-tree/pin.json
|
|
5
5
|
var pin_default = {
|
|
6
6
|
_: "THE CLOUD COMPUTER'S BROWSER FOR LINUX that this release installs (tools/browse-tree; make.sh writes it). The files are public at <base><tag>/; both sha256s are checked before anything is unpacked.",
|
|
7
|
-
tag: "
|
|
7
|
+
tag: "p2y",
|
|
8
8
|
base: "https://drwev5vxt2t2e.cloudfront.net/browse-tree/",
|
|
9
|
-
pcr0: "
|
|
10
|
-
commit: "
|
|
9
|
+
pcr0: "5a0c2c755715eae50b73fdc13856a6df963bf772e68db5f0155e729c3d822807684f3df10945430f7f75c2e36419e0a0",
|
|
10
|
+
commit: "f45e85eb16a84c90b126915ba8ca8914e2204650",
|
|
11
11
|
tree: {
|
|
12
12
|
file: "tree.tar.zst",
|
|
13
|
-
sha256: "
|
|
14
|
-
size:
|
|
13
|
+
sha256: "d992f07bbfec50467d9a462a4d443f714332d57cf7dc5a928eb9bde4bd023fc2",
|
|
14
|
+
size: 664651711
|
|
15
15
|
},
|
|
16
16
|
lab: {
|
|
17
17
|
file: "ccvm-lab.bin",
|
{witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-browser.js
RENAMED
|
@@ -43,6 +43,7 @@ function render (j) {
|
|
|
43
43
|
if (!j) return 'The browser did not answer.'
|
|
44
44
|
if (j.error) return 'Browser: ' + j.error
|
|
45
45
|
if (j.closed) return 'The browser is closed — its tabs and any sign-ins are gone. browser_open or browser_navigate starts a fresh one.'
|
|
46
|
+
if (j.shot && j.path) return 'Saved a picture of the page: ' + j.path + ' — the person sees it under your answer.'
|
|
46
47
|
if (j.cleared) return 'Cleared: the browser\'s history, cookies, cache and site data are gone, and a fresh browser is open — browser_navigate to go on.'
|
|
47
48
|
if (j.ok && !j.title && !j.url && !(j.elements && j.elements.length) && !j.path) return 'The browser is open' + (j.headed === false ? ' (headless — see it with browser_screenshot)' : j.headed ? ' (a window on this computer\'s screen)' : '') + '. Navigate to a URL next.' // the open op: nothing on screen yet
|
|
48
49
|
const head = [j.title ? 'Title: ' + j.title : null, j.url ? 'URL: ' + j.url : null].filter(Boolean).join('\n')
|
|
@@ -157,7 +158,7 @@ export const WitbitzBrowser = async (ctx = {}) => {
|
|
|
157
158
|
browser_screenshot: tool({
|
|
158
159
|
description: 'Save a picture of the page for the PERSON to look at, and return its path. Do not use this to decide what to click — use a snapshot.',
|
|
159
160
|
args: {},
|
|
160
|
-
execute: () => run('screenshot', {}),
|
|
161
|
+
execute: (args, context) => run('screenshot', { session: (context && context.sessionID) || '' }),
|
|
161
162
|
}),
|
|
162
163
|
},
|
|
163
164
|
}
|
|
@@ -72,15 +72,17 @@ def _answer(st: int, o: dict) -> tuple[int, str]:
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
def serve_output(*, directory: Any, path: Any, stat: bool = False, max_bytes: int = MAX_SERVE_BYTES,
|
|
75
|
-
any_kind: bool = False) -> tuple[int, str]:
|
|
75
|
+
any_kind: bool = False, shots: str = "") -> tuple[int, str]:
|
|
76
76
|
"""One produced file (or its facts, with `stat`) for the page: (status, JSON body) in the connector's reply shape.
|
|
77
77
|
|
|
78
78
|
`any_kind`: the Files sheet's Download (files.py) — any regular file, not only a kind the page previews; every other
|
|
79
|
-
check stays (⇄ tools/code-outputs.mjs).
|
|
79
|
+
check stays (⇄ tools/code-outputs.mjs). `shots`: the session's screenshots folder (<home>/.witbitz/screens/<session>,
|
|
80
|
+
browse-cdp.mjs) — served beside its folder with every same check."""
|
|
80
81
|
if not isinstance(directory, str) or not directory.startswith("/") or not valid_output_path(path):
|
|
81
82
|
return _answer(400, {"error": "not a file in this session"})
|
|
82
|
-
d = re.sub(r"/+\Z", "", posix_normalize(directory))
|
|
83
83
|
a = posix_normalize(path)
|
|
84
|
+
shot_dir = re.sub(r"/+\Z", "", posix_normalize(shots)) if isinstance(shots, str) and shots.startswith("/") else ""
|
|
85
|
+
d = shot_dir if shot_dir and a.startswith(shot_dir + "/") else re.sub(r"/+\Z", "", posix_normalize(directory))
|
|
84
86
|
if not d or not a.startswith(d + "/"):
|
|
85
87
|
return _answer(403, {"error": "that file is outside the session's folder"})
|
|
86
88
|
if _sensitive_path(a[len(d):]):
|
|
@@ -229,7 +229,7 @@ class Manager:
|
|
|
229
229
|
for p in ps:
|
|
230
230
|
paired.append({"name": p.get("name") or "", "account": p.get("account") or "", "port": wizard.port_of(p), "keys": await self.keys(p),
|
|
231
231
|
"computer": p.get("computerId") or "", "app": pp.app_label(p)})
|
|
232
|
-
pair = {k: v for k, v in self.pair_state.items() if k != "log"}
|
|
232
|
+
pair = {k: v for k, v in self.pair_state.items() if k != "log" and (k != "code" or self.pair_state.get("state") == "waiting")}
|
|
233
233
|
serving = self.serving()
|
|
234
234
|
if serving["state"] == "stopped" and ps:
|
|
235
235
|
el = await self.elsewhere()
|
|
@@ -354,7 +354,8 @@ class Manager:
|
|
|
354
354
|
ch = devicelink.new_link_challenge()
|
|
355
355
|
self.qr_svg = qr.qr_svg(ch.text)
|
|
356
356
|
n = int(self.pair_state.get("n", 0)) + 1
|
|
357
|
-
|
|
357
|
+
# The code as text beside the QR: a computer cannot scan its own screen, and Add a device takes it pasted.
|
|
358
|
+
self.pair_state = {"state": "waiting", "n": n, "until": int(time.time() * 1000) + link.LIFE_MS, "message": "", "code": ch.text}
|
|
358
359
|
self.pair_task = asyncio.ensure_future(self._pair_run(ch, await self._pair_port()))
|
|
359
360
|
return {"ok": True}
|
|
360
361
|
|
|
@@ -50,6 +50,7 @@ footer { font-size: 13px; text-align: center; }
|
|
|
50
50
|
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
|
51
51
|
.actions:not(:has(button:not([hidden]))) { display: none; }
|
|
52
52
|
button.small { font-size: 13px; padding: 2px 10px; margin-left: 8px; }
|
|
53
|
+
pre.cmd.code { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; user-select: all; }
|
|
53
54
|
pre.cmd { background: var(--off-bg); color: var(--text); padding: 8px 10px; border-radius: 8px; }
|
|
54
55
|
/* the security check (seccheck.py) */
|
|
55
56
|
p.small, div.small { font-size: 12.5px; }
|
|
@@ -92,11 +92,14 @@ function render(s) {
|
|
|
92
92
|
const want = '/api/pair/qr.svg?n=' + s.pair.n
|
|
93
93
|
if ($('qr').getAttribute('src') !== want) $('qr').setAttribute('src', want)
|
|
94
94
|
show($('qr').parentElement, true)
|
|
95
|
+
$('pairCode').textContent = s.pair.code || ''
|
|
96
|
+
show($('codeBox'), !!s.pair.code)
|
|
95
97
|
const left = Math.max(0, Math.round((s.pair.until - Date.now()) / 60000))
|
|
96
98
|
$('pairMsg').textContent = `Waiting for your phone… (this code works for ${left || 'under a'} minute${left === 1 ? '' : 's'})`
|
|
97
99
|
show($('pairRetry'), false)
|
|
98
100
|
} else if (s.pair.state === 'failed') {
|
|
99
101
|
show($('qr').parentElement, false)
|
|
102
|
+
show($('codeBox'), false)
|
|
100
103
|
$('pairMsg').textContent = s.pair.message || 'Pairing did not finish.'
|
|
101
104
|
show($('pairRetry'), true)
|
|
102
105
|
}
|
|
@@ -306,6 +309,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
306
309
|
// same OpenCode); nothing is unpaired. Code here pauses while the code is up and comes back when it is scanned or cancelled.
|
|
307
310
|
$('pairAdd').onclick = () => act('/api/pair', {}, 'Add another account? A new code appears — scan it from the other account: Code in Spaces → Computers → Add a computer. Code on this computer pauses until it is scanned or cancelled.')
|
|
308
311
|
$('pairCancel').onclick = () => act('/api/pair/cancel', {})
|
|
312
|
+
$('pairCopy').onclick = async () => {
|
|
313
|
+
const b = $('pairCopy')
|
|
314
|
+
try { await navigator.clipboard.writeText($('pairCode').textContent); b.textContent = 'Copied ✓' } catch { b.textContent = 'Select the code above and copy it' }
|
|
315
|
+
setTimeout(() => { b.textContent = 'Copy the code' }, 2000)
|
|
316
|
+
}
|
|
309
317
|
$('pairRetry').onclick = () => act('/api/pair', {})
|
|
310
318
|
$('login').onchange = (e) => act('/api/login', { on: e.target.checked })
|
|
311
319
|
refresh(true)
|
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
<li>Scan this code with the account that should reach this computer.</li>
|
|
46
46
|
</ol>
|
|
47
47
|
<div class="qr-wrap"><img id="qr" alt="Pairing code — scan it in Spaces" width="260" height="260"></div>
|
|
48
|
+
<div id="codeBox" hidden>
|
|
49
|
+
<p class="muted">On this computer? Copy the code and paste it under <b>…or paste the code</b> in Add a device.</p>
|
|
50
|
+
<pre id="pairCode" class="cmd code"></pre>
|
|
51
|
+
<div class="actions"><button id="pairCopy" type="button" class="ghost">Copy the code</button></div>
|
|
52
|
+
</div>
|
|
48
53
|
<p id="pairMsg" class="muted">Waiting for your phone…</p>
|
|
49
54
|
<div class="actions"><button id="pairCancel" type="button" class="ghost">Cancel</button><button id="pairRetry" type="button" hidden>Show a new code</button></div>
|
|
50
55
|
</section>
|
|
@@ -67,11 +67,30 @@ def _requests(d: Path, outside: Path, root: Path) -> list[dict]:
|
|
|
67
67
|
{"directory": str(d), "path": str(d / "out" / "big.pdf"), "stat": False, "maxBytes": 1024},
|
|
68
68
|
{"directory": str(d), "path": str(d / "out" / "big.pdf"), "stat": True, "maxBytes": 1024},
|
|
69
69
|
{"directory": str(d), "path": None, "stat": False}]
|
|
70
|
+
# the agent's screenshots: <home>/.witbitz/screens/<session>, served for that session only (2026-10-25)
|
|
71
|
+
mine, theirs = root / "screens" / "ses_A", root / "screens" / "ses_B"
|
|
72
|
+
reqs += [{"directory": str(d), "path": str(mine / "screen-1.jpg"), "stat": False, "shots": str(mine)},
|
|
73
|
+
{"directory": str(d), "path": str(theirs / "screen-2.jpg"), "stat": False, "shots": str(mine)},
|
|
74
|
+
{"directory": str(d), "path": str(mine / "screen-1.jpg"), "stat": False},
|
|
75
|
+
{"directory": str(d), "path": str(mine / "out.pdf"), "stat": False, "shots": str(mine)},
|
|
76
|
+
{"directory": str(d), "path": str(d / "out" / "chart.png"), "stat": False, "shots": str(mine)}]
|
|
70
77
|
return reqs
|
|
71
78
|
|
|
72
79
|
|
|
80
|
+
def _screens(root: Path, outside: Path) -> None:
|
|
81
|
+
mine, theirs = root / "screens" / "ses_A", root / "screens" / "ses_B"
|
|
82
|
+
mine.mkdir(parents=True)
|
|
83
|
+
theirs.mkdir(parents=True)
|
|
84
|
+
for p, b in [(mine / "screen-1.jpg", b"JPEGA"), (theirs / "screen-2.jpg", b"JPEGB")]:
|
|
85
|
+
p.write_bytes(b)
|
|
86
|
+
os.utime(p, (1789000000, 1789000000))
|
|
87
|
+
(mine / "out.pdf").symlink_to(outside / "report.pdf")
|
|
88
|
+
|
|
89
|
+
|
|
73
90
|
def _py(r: dict) -> list:
|
|
74
91
|
kw = {"max_bytes": r["maxBytes"]} if "maxBytes" in r else {}
|
|
92
|
+
if "shots" in r:
|
|
93
|
+
kw["shots"] = r["shots"]
|
|
75
94
|
st, b = serve_output(directory=r["directory"], path=r["path"], stat=r["stat"], **kw)
|
|
76
95
|
return [st, json.loads(b)]
|
|
77
96
|
|
|
@@ -92,12 +111,13 @@ def test_what_the_python_connector_serves(tmp_path):
|
|
|
92
111
|
@requires_node
|
|
93
112
|
def test_parity_with_the_js_connector(tmp_path):
|
|
94
113
|
d, outside = _tree(tmp_path)
|
|
114
|
+
_screens(tmp_path, outside)
|
|
95
115
|
reqs = _requests(d, outside, tmp_path)
|
|
96
116
|
js = run_node("""
|
|
97
117
|
import { serveOutput } from '@TOOLS@/code-outputs.mjs'
|
|
98
118
|
import { outputMime } from '@PUBLIC@/codeOutputs.js'
|
|
99
119
|
OUT({
|
|
100
|
-
served: INPUT.reqs.map((r) => { const o = serveOutput({ directory: r.directory, path: r.path, stat: r.stat, ...(r.maxBytes ? { maxBytes: r.maxBytes } : {}) }); return [o.st, JSON.parse(o.b)] }),
|
|
120
|
+
served: INPUT.reqs.map((r) => { const o = serveOutput({ directory: r.directory, path: r.path, stat: r.stat, ...(r.maxBytes ? { maxBytes: r.maxBytes } : {}), ...(r.shots ? { shots: r.shots } : {}) }); return [o.st, JSON.parse(o.b)] }),
|
|
101
121
|
mimes: INPUT.exts.map((e) => outputMime('/a/x.' + e)),
|
|
102
122
|
})
|
|
103
123
|
""", {"reqs": reqs, "exts": EXTS})
|
|
@@ -108,3 +128,12 @@ def test_parity_with_the_js_connector(tmp_path):
|
|
|
108
128
|
|
|
109
129
|
EXTS = ["pdf", "PNG", "jpg", "jpeg", "gif", "webp", "csv", "tsv", "md", "markdown", "txt", "docx", "doc", "xlsx", "xls", "pptx", "ppt",
|
|
110
130
|
"odt", "ods", "odp", "rtf", "zip", "epub", "heic", "svg", "html", "htm", "mp3", "wav", "m4a", "mp4", "mov", "ts", "json", ""]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_a_screenshot_is_served_for_its_own_session_only(tmp_path):
|
|
134
|
+
d, outside = _tree(tmp_path)
|
|
135
|
+
_screens(tmp_path, outside)
|
|
136
|
+
mine = tmp_path / "screens" / "ses_A"
|
|
137
|
+
assert _py({"directory": str(d), "path": str(mine / "screen-1.jpg"), "stat": False, "shots": str(mine)})[0] == 200
|
|
138
|
+
assert _py({"directory": str(d), "path": str(tmp_path / "screens" / "ses_B" / "screen-2.jpg"), "stat": False, "shots": str(mine)})[0] == 403
|
|
139
|
+
assert _py({"directory": str(d), "path": str(mine / "out.pdf"), "stat": False, "shots": str(mine)})[0] == 403, "a link out"
|
|
@@ -147,6 +147,7 @@ def test_unpair_then_pair_again_with_the_code_on_the_page_then_it_serves():
|
|
|
147
147
|
assert (await c.post(f"{base}/api/pair", headers=authed(srv), json={})).json() == {"ok": True}
|
|
148
148
|
st = (await c.get(f"{base}/api/status", headers=authed(srv))).json()
|
|
149
149
|
assert st["pair"]["state"] == "waiting" and st["pair"]["n"] == 1
|
|
150
|
+
assert st["pair"]["code"].startswith("wbzlink1:"), "the code as text too: a computer cannot scan its own screen, Add a device takes it pasted"
|
|
150
151
|
qr = await c.get(f"{base}/api/pair/qr.svg?n=1", headers={"cookie": f"wbui_{srv.port}=T0KEN"})
|
|
151
152
|
assert qr.status_code == 200 and qr.content.startswith(b"<svg"), "the code an <img> loads with the cookie alone"
|
|
152
153
|
assert (await c.get(f"{base}/api/pair/qr.svg")).status_code == 403
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-notes.js
RENAMED
|
File without changes
|
{witbitz_code-1.14.0 → witbitz_code-1.15.2}/src/witbitz_code/opencode_plugins/witbitz-progress.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|