@wenrwa/marketplace-cli 0.8.4 → 0.8.6

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 (42) hide show
  1. package/dist/__tests__/bridge-changes-report.test.js +25 -0
  2. package/dist/__tests__/bridge-changes-reporter.test.js +98 -0
  3. package/dist/__tests__/bridge-read-transcript-frame.test.js +22 -0
  4. package/dist/__tests__/bridge-save-env.test.d.ts +1 -0
  5. package/dist/__tests__/bridge-save-env.test.js +343 -0
  6. package/dist/__tests__/bridge-server-phantom-takeover.test.d.ts +1 -0
  7. package/dist/__tests__/bridge-server-phantom-takeover.test.js +84 -0
  8. package/dist/__tests__/bridge-server.test.js +9 -11
  9. package/dist/__tests__/secrets-egress.test.d.ts +1 -0
  10. package/dist/__tests__/secrets-egress.test.js +237 -0
  11. package/dist/__tests__/secrets-redact.test.d.ts +1 -0
  12. package/dist/__tests__/secrets-redact.test.js +318 -0
  13. package/dist/__tests__/term-snapshot.test.js +136 -0
  14. package/dist/bridge/changesReport.js +10 -3
  15. package/dist/bridge/changesReporter.js +0 -0
  16. package/dist/bridge/contextWriter.js +5 -1
  17. package/dist/bridge/envSaver.d.ts +63 -0
  18. package/dist/bridge/envSaver.js +256 -0
  19. package/dist/bridge/hiveopSettings.js +5 -0
  20. package/dist/bridge/launchSupervisor.js +4 -1
  21. package/dist/bridge/localContextWriter.js +6 -2
  22. package/dist/bridge/paneClient.d.ts +32 -0
  23. package/dist/bridge/paneClient.js +30 -0
  24. package/dist/bridge/realPty.js +5 -1
  25. package/dist/bridge/relayClient.d.ts +0 -18
  26. package/dist/bridge/relayClient.js +21 -6
  27. package/dist/bridge/repoEnv.d.ts +34 -0
  28. package/dist/bridge/repoEnv.js +186 -0
  29. package/dist/bridge/server.js +69 -26
  30. package/dist/bridge/sessionManager.d.ts +4 -0
  31. package/dist/bridge/sessionManager.js +28 -8
  32. package/dist/bridge/termSnapshot.js +114 -2
  33. package/dist/bridge/workGraphPointer.d.ts +25 -0
  34. package/dist/bridge/workGraphPointer.js +36 -1
  35. package/dist/commands/bridge.js +4 -0
  36. package/dist/secrets/console.d.ts +4 -0
  37. package/dist/secrets/console.js +34 -0
  38. package/dist/secrets/redact.d.ts +79 -0
  39. package/dist/secrets/redact.js +637 -0
  40. package/hooks/opencode-plugin.mjs +55 -2
  41. package/hooks/pre-tool-use.js +96 -0
  42. package/package.json +1 -1
@@ -36,7 +36,10 @@ function createTermSnapshot(cols, rows, opts) {
36
36
  // stable public API).
37
37
  const term = new Terminal({ cols, rows, scrollback, allowProposedApi: true });
38
38
  const serializer = new SerializeAddon();
39
- term.loadAddon(serializer);
39
+ // The addon reads the terminal through a CLIPPED VIEW, not the terminal
40
+ // itself — see clippedView below. activate() only stores the reference
41
+ // (the addon has no other loadAddon-time wiring); dispose() releases it.
42
+ serializer.activate(clippedView(term));
40
43
  // Terminal.write() is async: the parser runs on an internal write queue
41
44
  // and only calls the callback once that write has actually been applied
42
45
  // to the buffer. Resolving our promise from that callback means callers
@@ -56,7 +59,7 @@ function createTermSnapshot(cols, rows, opts) {
56
59
  },
57
60
  serialize: async () => {
58
61
  await flush();
59
- return serializer.serialize({ scrollback });
62
+ return serializer.serialize({ scrollback }) + extraModes(term);
60
63
  },
61
64
  dispose: () => {
62
65
  serializer.dispose();
@@ -64,3 +67,112 @@ function createTermSnapshot(cols, rows, opts) {
64
67
  },
65
68
  };
66
69
  }
70
+ // ---------------------------------------------------------------------------
71
+ // Narrow-after-wide clipping.
72
+ //
73
+ // xterm does not truncate buffer lines when the terminal shrinks: the
74
+ // alternate buffer (where full-screen TUIs like OpenCode draw) never reflows,
75
+ // so after a 40→20 col shrink every alt row still holds 40 cells. The
76
+ // serialize addon walks `line.length` cells per row, so the snapshot wrote
77
+ // each row at its OLD width, the replay terminal (at the new width)
78
+ // auto-wrapped the tail onto the next row, and the frame came back garbled
79
+ // on reattach. The live screen only ever shows the first `cols` cells, so the
80
+ // snapshot clips every row to `cols` — via a read-only view, never by
81
+ // mutating the emulator (growing back must still reveal what xterm kept).
82
+ // ---------------------------------------------------------------------------
83
+ /* eslint-disable @typescript-eslint/no-explicit-any */
84
+ /** Forward every property of `target`, with functions bound to it, except the overrides. */
85
+ function forwardingProxy(target, override) {
86
+ return new Proxy(target, {
87
+ get(t, prop) {
88
+ const o = override(prop);
89
+ if (o)
90
+ return o.value;
91
+ const v = Reflect.get(t, prop, t);
92
+ return typeof v === 'function' ? v.bind(t) : v;
93
+ },
94
+ });
95
+ }
96
+ /** The number of cells of `line` that are visible at `cols` — never splits a
97
+ * wide (2-cell) character: one that straddles the edge is dropped whole. */
98
+ function visibleLength(line, cols) {
99
+ if (!line || line.length <= cols)
100
+ return line ? line.length : 0;
101
+ const edge = line.getCell(cols - 1);
102
+ return edge && edge.getWidth() === 2 ? cols - 1 : cols;
103
+ }
104
+ function clippedBuffer(buffer, term) {
105
+ return forwardingProxy(buffer, (prop) => {
106
+ if (prop !== 'getLine')
107
+ return null;
108
+ return {
109
+ hit: true,
110
+ value: (y) => {
111
+ const line = buffer.getLine(y);
112
+ if (!line)
113
+ return line;
114
+ const cols = term.cols;
115
+ const len = visibleLength(line, cols);
116
+ // A row that continued a now-clipped wider row is no longer a soft
117
+ // wrap of it at this width — the serializer must end the previous
118
+ // row with a hard CRLF, not rely on auto-wrap.
119
+ const prev = y > 0 ? buffer.getLine(y - 1) : undefined;
120
+ const wrapped = line.isWrapped && !(prev && prev.length > cols);
121
+ if (len === line.length && wrapped === line.isWrapped)
122
+ return line;
123
+ return forwardingProxy(line, (p) => {
124
+ if (p === 'length')
125
+ return { hit: true, value: len };
126
+ if (p === 'isWrapped')
127
+ return { hit: true, value: wrapped };
128
+ return null;
129
+ });
130
+ },
131
+ };
132
+ });
133
+ }
134
+ /** The terminal as the serialize addon sees it: same object, but every
135
+ * buffer row is clipped to the CURRENT cols. Rows need no clipping: the
136
+ * alt buffer is exactly `rows` long after a resize, and the normal buffer
137
+ * pushes the overflow into scrollback (which the replay scrolls too). */
138
+ function clippedView(term) {
139
+ // Resolved NOW, not on first serialize: xterm creates + registers the
140
+ // buffer namespace lazily, and doing that on a terminal already disposed
141
+ // (a serialize racing dispose) leaks a disposable with a warning.
142
+ const ns = term.buffer;
143
+ const view = forwardingProxy(ns, (prop) => {
144
+ if (prop === 'normal' || prop === 'alternate' || prop === 'active') {
145
+ return { hit: true, value: clippedBuffer(ns[prop], term) };
146
+ }
147
+ return null;
148
+ });
149
+ return forwardingProxy(term, (prop) => (prop === 'buffer' ? { hit: true, value: view } : null));
150
+ }
151
+ // ---------------------------------------------------------------------------
152
+ // Terminal modes the serialize addon drops.
153
+ //
154
+ // The addon re-emits mouse TRACKING (?9/?1000/?1002/?1003), bracketed paste,
155
+ // application cursor/keypad, focus, insert/origin/wrap and the alt screen
156
+ // (?1049) — but not the mouse ENCODING (?1006 SGR / ?1016 SGR-pixels) nor a
157
+ // hidden cursor (?25l). Without ?1006 a reattached client reports wheel and
158
+ // clicks in the legacy X10 byte encoding, which an SGR-expecting TUI
159
+ // (OpenCode) ignores — so mouse-wheel scroll silently died after a reattach,
160
+ // and the hidden cursor reappeared as a stray block.
161
+ //
162
+ // Read from the emulator's own state (the same `_core` internals the addon
163
+ // itself reads), so resets (RIS/DECSTR) are honoured exactly as xterm
164
+ // applies them — no shadow parser that could drift.
165
+ // ---------------------------------------------------------------------------
166
+ /** Escape sequences restoring the modes the serialize addon does not emit. */
167
+ function extraModes(term) {
168
+ const core = term?._core;
169
+ let out = '';
170
+ const encoding = core?.coreMouseService?.activeEncoding;
171
+ if (encoding === 'SGR')
172
+ out += '\x1b[?1006h';
173
+ else if (encoding === 'SGR_PIXELS')
174
+ out += '\x1b[?1016h';
175
+ if (core?.coreService?.isCursorHidden === true)
176
+ out += '\x1b[?25l';
177
+ return out;
178
+ }
@@ -67,3 +67,28 @@ export declare const BROWSER_POINTER: string;
67
67
  export declare function browserSection(input: {
68
68
  workGraphAvailable?: boolean;
69
69
  }): string | null;
70
+ /**
71
+ * Paste a key → saved to .env (2026-10-17) — keys a pilot pastes into a pane
72
+ * are saved to the repo's `.env` and the pane types `$NAME` instead. New PTYs
73
+ * load that file into env (repoEnv.ts); an agent that was already running
74
+ * needs to be told where the key went. Same gate as the other HiveOp pointers
75
+ * (a HiveOp-managed agent).
76
+ */
77
+ export declare const KEYS_POINTER: string;
78
+ /** One function so the hosted and local writers cannot diverge. */
79
+ export declare function keysSection(input: {
80
+ workGraphAvailable?: boolean;
81
+ }): string | null;
82
+ /**
83
+ * Secret masking (2026-10-17) — the standing rule every HiveOp agent gets:
84
+ * pilots stream and screenshot their terminals, so a secret printed there is
85
+ * a secret leaked. Output masking and the PreToolUse guard are the safety net;
86
+ * this is the instruction that keeps the agent from needing them.
87
+ *
88
+ * Rides the pointer block whenever it is written at all (both lanes), so it
89
+ * never on its own creates a file or a CLAUDE.md region that would otherwise
90
+ * not exist.
91
+ */
92
+ export declare const SECRETS_POINTER: string;
93
+ /** The pointer list with the secrets rule appended — one function so both writers agree. */
94
+ export declare function withSecretsSection(sections: string[]): string[];
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BROWSER_POINTER = exports.MATCH_POINTER = exports.WORK_GRAPH_POINTER = void 0;
3
+ exports.SECRETS_POINTER = exports.KEYS_POINTER = exports.BROWSER_POINTER = exports.MATCH_POINTER = exports.WORK_GRAPH_POINTER = void 0;
4
4
  exports.workSection = workSection;
5
5
  exports.browserSection = browserSection;
6
+ exports.keysSection = keysSection;
7
+ exports.withSecretsSection = withSecretsSection;
6
8
  /**
7
9
  * wgcp-01 — the one piece of guidance that tells an agent the workspace work
8
10
  * graph exists at all. Nothing today does: the MCP tools (`list_ready_issues`,
@@ -84,3 +86,36 @@ exports.BROWSER_POINTER = [
84
86
  function browserSection(input) {
85
87
  return input.workGraphAvailable ? exports.BROWSER_POINTER : null;
86
88
  }
89
+ /**
90
+ * Paste a key → saved to .env (2026-10-17) — keys a pilot pastes into a pane
91
+ * are saved to the repo's `.env` and the pane types `$NAME` instead. New PTYs
92
+ * load that file into env (repoEnv.ts); an agent that was already running
93
+ * needs to be told where the key went. Same gate as the other HiveOp pointers
94
+ * (a HiveOp-managed agent).
95
+ */
96
+ exports.KEYS_POINTER = [
97
+ '## Keys',
98
+ 'Keys live in the repo .env; use them as env vars.',
99
+ ].join('\n');
100
+ /** One function so the hosted and local writers cannot diverge. */
101
+ function keysSection(input) {
102
+ return input.workGraphAvailable ? exports.KEYS_POINTER : null;
103
+ }
104
+ /**
105
+ * Secret masking (2026-10-17) — the standing rule every HiveOp agent gets:
106
+ * pilots stream and screenshot their terminals, so a secret printed there is
107
+ * a secret leaked. Output masking and the PreToolUse guard are the safety net;
108
+ * this is the instruction that keeps the agent from needing them.
109
+ *
110
+ * Rides the pointer block whenever it is written at all (both lanes), so it
111
+ * never on its own creates a file or a CLAUDE.md region that would otherwise
112
+ * not exist.
113
+ */
114
+ exports.SECRETS_POINTER = [
115
+ '## Secrets',
116
+ 'Never print, echo or cat secret values (tokens, keys, passwords) or paste them into commands. Keep them in env vars or .env and reference them by name, e.g. `$RAILWAY_TOKEN`.',
117
+ ].join('\n');
118
+ /** The pointer list with the secrets rule appended — one function so both writers agree. */
119
+ function withSecretsSection(sections) {
120
+ return sections.length > 0 ? [...sections, exports.SECRETS_POINTER] : sections;
121
+ }
@@ -80,6 +80,7 @@ const contextWriter_1 = require("../bridge/contextWriter");
80
80
  const memoryBrief_1 = require("../bridge/memoryBrief");
81
81
  const containerFileRelay_1 = require("../bridge/containerFileRelay");
82
82
  const claudeTranscriptReader_1 = require("../bridge/claudeTranscriptReader");
83
+ const console_1 = require("../secrets/console");
83
84
  const config_2 = require("../utils/config");
84
85
  const deviceRegistration_1 = require("../bridge/deviceRegistration");
85
86
  const pair_1 = require("./pair");
@@ -524,6 +525,9 @@ exports.bridgeCommand = new commander_1.Command('bridge')
524
525
  // mode is a long-lived user process too, and deserves the same loud,
525
526
  // deliberate crash instead of a silent Node kill).
526
527
  installBridgeCrashHandlers();
528
+ // Secret masking (2026-10-17): every console line this process writes is
529
+ // bridge.log (launchd) or `fly logs` (container) — mask it at the source.
530
+ (0, console_1.installConsoleRedaction)();
527
531
  // ht-05: relay mode dials OUT to the platform relay instead of listening
528
532
  // on localhost. It skips the Connect Token / MCP registration / discovery
529
533
  // file entirely — the container image handles claude/MCP config (ht-06).
@@ -0,0 +1,4 @@
1
+ declare const METHODS: readonly ["log", "info", "warn", "error", "debug"];
2
+ type ConsoleLike = Pick<Console, (typeof METHODS)[number]>;
3
+ export declare function installConsoleRedaction(target?: ConsoleLike): void;
4
+ export {};
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installConsoleRedaction = installConsoleRedaction;
4
+ /**
5
+ * Secret masking for the bridge's own logs (2026-10-17). The bridge's stdout
6
+ * and stderr ARE ~/.wen-market/bridge.log on a laptop (launchd) and `fly
7
+ * logs` in a container, so every console line is passed through redact()
8
+ * before it is written. Installed once, at bridge start.
9
+ */
10
+ const util_1 = require("util");
11
+ const redact_1 = require("./redact");
12
+ const INSTALLED = Symbol.for('hiveop.consoleRedaction');
13
+ const METHODS = ['log', 'info', 'warn', 'error', 'debug'];
14
+ function installConsoleRedaction(target = console) {
15
+ const flagged = target;
16
+ if (flagged[INSTALLED])
17
+ return;
18
+ flagged[INSTALLED] = true;
19
+ for (const m of METHODS) {
20
+ const original = target[m].bind(target);
21
+ target[m] = (...args) => {
22
+ let line;
23
+ try {
24
+ line = (0, redact_1.redact)((0, util_1.format)(...args));
25
+ }
26
+ catch {
27
+ // Fail closed: an unmasked line could carry the very secret.
28
+ original('[log line withheld: secret masking failed]');
29
+ return;
30
+ }
31
+ original(line);
32
+ };
33
+ }
34
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Secret masking for terminal output (owner-approved 2026-10-17).
3
+ *
4
+ * A secret pasted or printed in any HiveOp terminal must never reach anyone's
5
+ * screen — the owner's own view included (they stream and screenshot). The PTY
6
+ * child still gets the real bytes; only what is DISPLAYED or SENT is masked.
7
+ *
8
+ * ONE SOURCE OF TRUTH. backend/src/lib/secrets/redact.ts is a byte-identical
9
+ * copy (the backend cannot import from cli/), and a backend test fails the
10
+ * moment the two differ. Edit this file, then copy it over.
11
+ *
12
+ * Exports:
13
+ * - findSecrets(text) spans a plain string would mask
14
+ * - containsSecret(text) true when findSecrets finds anything
15
+ * - redact(text) one-shot, ANSI-aware masking of a whole string
16
+ * - createStreamRedactor(opts) the same for a chunked PTY stream
17
+ *
18
+ * Masking is LENGTH-PRESERVING: every hidden character becomes one '*'. That
19
+ * keeps every column a TUI drew where it drew it (a shorter line would leave
20
+ * stale cells behind a cursor-addressed repaint), keeps patch byte sizes, and
21
+ * lets a value that arrives in two pieces be masked piece by piece. Prefixed
22
+ * tokens keep their first 4 characters (`npm_****…`); a NAME=value keeps the
23
+ * name (`RAILWAY_API_TOKEN=****…`). '*' is ASCII, so it is one cell wide in
24
+ * every terminal, and a re-scan of masked text finds nothing (idempotent).
25
+ *
26
+ * Streaming: output arrives in arbitrary chunks, interleaved with escape
27
+ * sequences. Escapes pass through untouched (OSC titles are masked inside);
28
+ * text is matched on a "visible" view with SGR colour codes removed, so a
29
+ * token painted in two colours is still one token. A tail that could still be
30
+ * the start of a secret is held back (bounded) and released on the next chunk
31
+ * or after a short idle, so interactive echo stays snappy.
32
+ */
33
+ export declare const MASK_CHAR = "*";
34
+ export type SecretKind = 'prefixed' | 'assignment' | 'bearer' | 'url-credential' | 'private-key';
35
+ export interface SecretSpan {
36
+ /** Index of the first character that is masked. */
37
+ start: number;
38
+ /** One past the last masked character. */
39
+ end: number;
40
+ kind: SecretKind;
41
+ }
42
+ /** True when an identifier names a secret: RAILWAY_API_TOKEN, apiKey, --password, x-api-key. */
43
+ export declare function isSecretName(name: string): boolean;
44
+ /** Spans a plain (escape-free) string would mask, sorted and merged. */
45
+ export declare function findSecrets(text: string): SecretSpan[];
46
+ /** True when `text` holds anything findSecrets would mask. */
47
+ export declare function containsSecret(text: string): boolean;
48
+ export interface StreamRedactorOptions {
49
+ /** Receives masked output, in order. Never called with an empty string. */
50
+ emit: (masked: string) => void;
51
+ /** Idle time before a held tail is released (default 30ms; keep ≤50). */
52
+ idleMs?: number;
53
+ /** Most visible characters ever held back (default 512). */
54
+ maxHold?: number;
55
+ /** On an idle flush, also mask a value still being typed after a secret
56
+ * name (`export API_TOKEN=ab12…` with no newline yet). Default true; the
57
+ * one-shot redact() turns it off, since its input is complete. */
58
+ maskTrailing?: boolean;
59
+ /** Injectable timers (tests). */
60
+ setTimer?: (cb: () => void, ms: number) => unknown;
61
+ clearTimer?: (handle: unknown) => void;
62
+ }
63
+ export interface StreamRedactor {
64
+ /** Feed one chunk of raw output. */
65
+ push(chunk: string): void;
66
+ /** Release everything held, masking what is complete. */
67
+ flush(): void;
68
+ /** Flush and stop the idle timer. */
69
+ dispose(): void;
70
+ }
71
+ export declare function createStreamRedactor(opts: StreamRedactorOptions): StreamRedactor;
72
+ /** One-shot, ANSI-aware masking of a complete string (logs, replays, screens, files). */
73
+ export declare function redact(text: string): string;
74
+ /**
75
+ * redact() for large text (a multi-MB transcript) without blocking the event
76
+ * loop: the same stream redactor fed in slices, yielding between them so live
77
+ * terminal frames keep flowing. Same result as redact(text).
78
+ */
79
+ export declare function redactInSlices(text: string, sliceChars?: number): Promise<string>;