@staff0rd/assist 0.305.4 → 0.307.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -224,6 +224,8 @@ The first backlog command in a repository that still has a local `.assist/backlo
224
224
  - `assist sql mutate "<sql>" [connection]` - Execute a mutating SQL statement and print rows affected (rejects non-mutating statements like pure SELECTs)
225
225
  - `assist sql tables [connection]` - List tables in the connected database (via INFORMATION_SCHEMA.TABLES)
226
226
  - `assist sql columns <table> [connection]` - List columns for a table (use `schema.table` for non-default schema; via INFORMATION_SCHEMA.COLUMNS)
227
+ - `assist netcap [-p, --port <port>] [-o, --out <dir>] [-f, --filter <pattern>]` - Start a local receiver that captures browser network traffic to a JSONL file (`capture.jsonl` under `--out`, default `~/.assist/netcap`), paired with the netcap browser extension. Sends permissive CORS, logs each ping/capture live, and runs until Ctrl-C, then prints a capture count (default port `8723`). `--filter` bakes a URL substring into the extension so only matching requests forward. See [netcap browser extension](#netcap-browser-extension)
228
+ - `assist netcap extract [file]` - Parse a netcap capture file into structured posts (`text`, `markdown` with mentions linked, `author`, `mentions`, `hashtags`, `links`, `relatedPosts`, and `postedAt` decoded from the activity id) and write them to `posts.json` beside the capture (defaults to `~/.assist/netcap/capture.jsonl`). Reads both the LinkedIn SDUI (rsc-action) responses rendered on first paint and the voyager GraphQL profile-updates responses loaded on scroll, deduped by activity urn keeping the richest copy of each post
227
229
  - `assist screenshot <process>` - Capture a screenshot of a running application window (e.g. `assist screenshot notepad`). Output directory is configurable via `screenshot.outputDir` (default `./screenshots`)
228
230
  - `assist handover save --summary <s>` - Save a session handover note to the backlog DB (content read from stdin), scoped by the repo's git origin. Appends a new row each call; `--summary` is the one-line description shown when recalling
229
231
  - `assist handover list` - List unrecalled handovers for this repo, most recent first, one per line as tab-separated `id`, ISO-8601 created timestamp, and one-line summary. Prints nothing when none are pending
@@ -273,3 +275,16 @@ When iterating on assist itself: web server changes only need the `assist sessio
273
275
 
274
276
  When `commit.pull` is enabled in config, `assist draft`, `assist bug`, `assist refine`, `assist next`, and `assist backlog run` run `git pull --ff-only` before doing anything else; if the pull fails the command aborts. `assist next` pulls once per invocation, not per item in its loop.
275
277
 
278
+
279
+ ## netcap browser extension
280
+
281
+ `assist netcap` only runs the receiver; the browser side is a raw Manifest V3 extension (no build step) under `netcap-extension/`. A MAIN-world content script patches `fetch`/`XMLHttpRequest` to capture `{url, method, status, requestBody, responseBody, timestamp}`, relays each entry to the background service worker (`window.postMessage` → `chrome.runtime.sendMessage`), and the background worker POSTs it to the receiver. The XHR patch reads the response across every `responseType` (`json`, `blob`, `arraybuffer`, `document`, not just `text`) — LinkedIn's voyager GraphQL calls use `responseType: "json"`, which exposes no `responseText`, so reading `responseText` alone captured those bodies empty. Forwarding happens in the background context, so the page's CSP (`connect-src`) never blocks it — the limitation that killed the earlier console-paste approach.
282
+
283
+ 1. Run `assist netcap` — it prints the receiver URL, the capture file path, and the absolute path to the extension directory to load. The receiver host/port is baked into the extension's `background.js` at this point. Under WSL (where the browser runs on the Windows host, cannot load from a WSL path, and cannot reach the receiver on `localhost` because WSL2 localhost forwarding is unreliable) it copies the extension to `C:\tools\netcap-extension`, targets the WSL VM's IP, and prints that Windows path instead. Re-run `assist netcap` after a reboot (the WSL IP can change) and reload the extension.
284
+ 2. Load the unpacked extension:
285
+ - **Firefox**: open `about:debugging#/runtime/this-firefox` → **Load Temporary Add-on…** → pick `manifest.json` inside the printed extension directory. (Requires Firefox 128+ for MAIN-world content scripts.)
286
+ - **Chrome**: open `chrome://extensions`, enable **Developer mode**, click **Load unpacked**, and select the extension directory.
287
+ 3. On load the background worker pings the receiver; `ping from extension` appears in the `assist netcap` log, confirming browser→server connectivity.
288
+ 4. Browse a site (e.g. LinkedIn activity); matching requests append to the capture file live and survive page refreshes (re-loading the extension appends to the same file). Press Ctrl-C to stop the receiver; it prints how many entries were captured.
289
+
290
+ `assist netcap` bakes the receiver host/port and the optional `--filter` substring into the extension's `background.js` at startup, so you don't normally edit it by hand. With `--filter <pattern>`, the background worker only forwards requests whose URL contains `<pattern>` (case-sensitive substring); the matching logic mirrors `matchesNetcapFilter` in `src/commands/netcap`. Use `--out <dir>` to write `capture.jsonl` into a different directory.
@@ -0,0 +1,30 @@
1
+ // Background service worker: the only context that talks to the receiver. It
2
+ // runs outside any page, so the page's CSP does not restrict its fetches. On
3
+ // startup it pings the receiver (the connectivity spike) and thereafter
4
+ // forwards every captured entry relayed from the content scripts.
5
+ const RECEIVER = "http://127.0.0.1:8723/";
6
+ const FILTER = "";
7
+
8
+ function ping() {
9
+ fetch(`${RECEIVER}ping`, { method: "GET" }).catch(() => {});
10
+ }
11
+
12
+ // why: duplicates matchesNetcapFilter (src/commands/netcap) — plain JS, no TS import
13
+ function matchesFilter(url) {
14
+ if (!FILTER) return true;
15
+ return typeof url === "string" && url.includes(FILTER);
16
+ }
17
+
18
+ ping();
19
+ chrome.runtime.onInstalled?.addListener(ping);
20
+ chrome.runtime.onStartup?.addListener(ping);
21
+
22
+ chrome.runtime.onMessage.addListener((message) => {
23
+ if (!message || message.type !== "netcap-entry") return;
24
+ if (!matchesFilter(message.entry && message.entry.url)) return;
25
+ fetch(RECEIVER, {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify(message.entry),
29
+ }).catch(() => {});
30
+ });
@@ -0,0 +1,114 @@
1
+ // MAIN-world content script: runs in the page's own JS context so it can patch
2
+ // the page's fetch/XMLHttpRequest before page code uses them. Captured entries
3
+ // are handed to the isolated-world relay via window.postMessage; this script
4
+ // never talks to the receiver itself (the background worker does, outside CSP).
5
+ (() => {
6
+ const SOURCE = "assist-netcap";
7
+
8
+ function post(entry) {
9
+ try {
10
+ window.postMessage({ source: SOURCE, entry }, "*");
11
+ } catch {
12
+ // ignore: structured-clone failures on exotic bodies
13
+ }
14
+ }
15
+
16
+ function bodyToString(body) {
17
+ return typeof body === "string" ? body : null;
18
+ }
19
+
20
+ const originalFetch = window.fetch;
21
+ if (typeof originalFetch === "function") {
22
+ window.fetch = function (...args) {
23
+ const input = args[0];
24
+ const init = args[1] || {};
25
+ const url =
26
+ typeof input === "string" || input instanceof URL
27
+ ? String(input)
28
+ : input && input.url
29
+ ? input.url
30
+ : "";
31
+ const method = (
32
+ init.method ||
33
+ (input && input.method) ||
34
+ "GET"
35
+ ).toUpperCase();
36
+ const requestBody = bodyToString(init.body);
37
+ const result = originalFetch.apply(this, args);
38
+ result
39
+ .then((response) => {
40
+ response
41
+ .clone()
42
+ .text()
43
+ .then((responseBody) => {
44
+ post({
45
+ url,
46
+ method,
47
+ status: response.status,
48
+ requestBody,
49
+ responseBody,
50
+ timestamp: Date.now(),
51
+ });
52
+ })
53
+ .catch(() => {});
54
+ })
55
+ .catch(() => {});
56
+ return result;
57
+ };
58
+ }
59
+
60
+ // why: voyager GraphQL uses responseType "json"/"blob" (no responseText), so reading responseText alone captured them empty
61
+ function xhrResponseBody(xhr) {
62
+ try {
63
+ const type = xhr.responseType;
64
+ if (type === "" || type === "text") return xhr.responseText ?? "";
65
+ if (type === "json")
66
+ return xhr.response == null ? "" : JSON.stringify(xhr.response);
67
+ if (type === "arraybuffer")
68
+ return new TextDecoder().decode(xhr.response);
69
+ if (type === "document")
70
+ return xhr.response?.documentElement?.outerHTML ?? "";
71
+ if (type === "blob") return xhr.response;
72
+ } catch {
73
+ // ignore: response unavailable for this responseType
74
+ }
75
+ return "";
76
+ }
77
+
78
+ const OriginalXHR = window.XMLHttpRequest;
79
+ if (OriginalXHR) {
80
+ const open = OriginalXHR.prototype.open;
81
+ const send = OriginalXHR.prototype.send;
82
+ OriginalXHR.prototype.open = function (method, url, ...rest) {
83
+ this.__netcap = { method: String(method).toUpperCase(), url: String(url) };
84
+ return open.call(this, method, url, ...rest);
85
+ };
86
+ OriginalXHR.prototype.send = function (body) {
87
+ const meta = this.__netcap;
88
+ if (meta) {
89
+ meta.requestBody = bodyToString(body);
90
+ this.addEventListener("loadend", () => {
91
+ const emit = (responseBody) =>
92
+ post({
93
+ url: meta.url,
94
+ method: meta.method,
95
+ status: this.status,
96
+ requestBody: meta.requestBody ?? null,
97
+ responseBody,
98
+ timestamp: Date.now(),
99
+ });
100
+ const value = xhrResponseBody(this);
101
+ if (value instanceof Blob) {
102
+ value
103
+ .text()
104
+ .then(emit)
105
+ .catch(() => emit(""));
106
+ } else {
107
+ emit(value);
108
+ }
109
+ });
110
+ }
111
+ return send.call(this, body);
112
+ };
113
+ }
114
+ })();
@@ -0,0 +1,25 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "assist netcap",
4
+ "version": "1.0.0",
5
+ "description": "Captures fetch/XHR traffic and forwards it to the local assist netcap receiver.",
6
+ "host_permissions": ["http://*/*"],
7
+ "background": {
8
+ "service_worker": "background.js",
9
+ "scripts": ["background.js"]
10
+ },
11
+ "content_scripts": [
12
+ {
13
+ "matches": ["<all_urls>"],
14
+ "js": ["interceptor.js"],
15
+ "run_at": "document_start",
16
+ "world": "MAIN"
17
+ },
18
+ {
19
+ "matches": ["<all_urls>"],
20
+ "js": ["relay.js"],
21
+ "run_at": "document_start",
22
+ "world": "ISOLATED"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,14 @@
1
+ // Isolated-world content script: bridges the MAIN-world interceptor's
2
+ // window.postMessage events to the background service worker via runtime
3
+ // messaging. The background worker, not the page, posts to the receiver, so the
4
+ // page's CSP (connect-src) never applies.
5
+ window.addEventListener("message", (event) => {
6
+ if (event.source !== window) return;
7
+ const data = event.data;
8
+ if (!data || data.source !== "assist-netcap" || !data.entry) return;
9
+ try {
10
+ chrome.runtime.sendMessage({ type: "netcap-entry", entry: data.entry });
11
+ } catch {
12
+ // ignore: extension context invalidated (e.g. after reload)
13
+ }
14
+ });