@wenrwa/marketplace-cli 0.8.5 → 0.8.7
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/dist/__tests__/bridge-changes-report.test.js +25 -0
- package/dist/__tests__/bridge-process-cpu.test.d.ts +1 -0
- package/dist/__tests__/bridge-process-cpu.test.js +135 -0
- package/dist/__tests__/bridge-read-transcript-frame.test.js +22 -0
- package/dist/__tests__/bridge-save-env.test.d.ts +1 -0
- package/dist/__tests__/bridge-save-env.test.js +343 -0
- package/dist/__tests__/bridge-server-phantom-takeover.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-phantom-takeover.test.js +84 -0
- package/dist/__tests__/bridge-server.test.js +9 -11
- package/dist/__tests__/secrets-egress.test.d.ts +1 -0
- package/dist/__tests__/secrets-egress.test.js +237 -0
- package/dist/__tests__/secrets-redact.test.d.ts +1 -0
- package/dist/__tests__/secrets-redact.test.js +318 -0
- package/dist/bridge/changesReport.js +10 -3
- package/dist/bridge/contextWriter.js +5 -1
- package/dist/bridge/envSaver.d.ts +63 -0
- package/dist/bridge/envSaver.js +256 -0
- package/dist/bridge/hiveopSettings.js +5 -0
- package/dist/bridge/launchSupervisor.js +4 -1
- package/dist/bridge/localContextWriter.js +6 -2
- package/dist/bridge/paneClient.d.ts +32 -0
- package/dist/bridge/paneClient.js +30 -0
- package/dist/bridge/processCpu.d.ts +29 -0
- package/dist/bridge/processCpu.js +160 -0
- package/dist/bridge/realPty.js +6 -1
- package/dist/bridge/relayClient.d.ts +9 -18
- package/dist/bridge/relayClient.js +32 -7
- package/dist/bridge/repoEnv.d.ts +34 -0
- package/dist/bridge/repoEnv.js +186 -0
- package/dist/bridge/server.js +69 -26
- package/dist/bridge/sessionManager.d.ts +10 -0
- package/dist/bridge/sessionManager.js +33 -8
- package/dist/bridge/workGraphPointer.d.ts +25 -0
- package/dist/bridge/workGraphPointer.js +36 -1
- package/dist/commands/bridge.js +8 -0
- package/dist/secrets/console.d.ts +4 -0
- package/dist/secrets/console.js +34 -0
- package/dist/secrets/redact.d.ts +79 -0
- package/dist/secrets/redact.js +637 -0
- package/hooks/opencode-plugin.mjs +55 -2
- package/hooks/pre-tool-use.js +96 -0
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export declare const SAVE_ENV_NAME_RE: RegExp;
|
|
2
|
+
export declare const MAX_SAVE_ENV_VALUE_BYTES = 4096;
|
|
3
|
+
export declare const MAX_SAVE_ENV_ENTRIES = 32;
|
|
4
|
+
export interface EnvEntry {
|
|
5
|
+
name: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
export type SaveEnvValidation = {
|
|
9
|
+
ok: true;
|
|
10
|
+
entries: EnvEntry[];
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
/** Validate the frame's entries. Error text is user-facing and never quotes a value. */
|
|
16
|
+
export declare function validateEnvEntries(raw: unknown): SaveEnvValidation;
|
|
17
|
+
/** Serialize one value so repoEnv.parseDotenv reads back exactly the same string. */
|
|
18
|
+
export declare function formatDotenvValue(value: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Upsert entries into dotenv text: the first line for a NAME is replaced in
|
|
21
|
+
* place, later duplicates of it are dropped, new names are appended. Every
|
|
22
|
+
* other line (comments, unrelated keys) is preserved byte for byte.
|
|
23
|
+
*/
|
|
24
|
+
export declare function upsertDotenv(existing: string, entries: EnvEntry[]): string;
|
|
25
|
+
export interface EnvSaverDeps {
|
|
26
|
+
realpath: (p: string) => string;
|
|
27
|
+
/** null when the path does not exist. */
|
|
28
|
+
lstat: (p: string) => {
|
|
29
|
+
isFile(): boolean;
|
|
30
|
+
isDirectory(): boolean;
|
|
31
|
+
isSymbolicLink(): boolean;
|
|
32
|
+
} | null;
|
|
33
|
+
readFile: (p: string) => string;
|
|
34
|
+
/** Temp file (mode 0600) + rename, then chmod 0600. */
|
|
35
|
+
writeFileAtomic0600: (p: string, data: string) => void;
|
|
36
|
+
appendFile: (p: string, data: string) => void;
|
|
37
|
+
/** Exit code of `git <args>` run in `cwd`; 128 (or any non 0/1) when git is unusable. */
|
|
38
|
+
git: (args: string[], cwd: string) => Promise<number>;
|
|
39
|
+
}
|
|
40
|
+
export declare const realEnvSaverDeps: EnvSaverDeps;
|
|
41
|
+
export type SaveEnvResult = {
|
|
42
|
+
ok: true;
|
|
43
|
+
names: string[];
|
|
44
|
+
gitignoreAdded: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
/** Write entries into `<root>/.env`. `root` is the session's cwd. */
|
|
50
|
+
export declare function saveEnvEntries(root: string, entries: EnvEntry[], deps?: EnvSaverDeps): Promise<SaveEnvResult>;
|
|
51
|
+
/**
|
|
52
|
+
* The whole `save-env` frame → `save-env-result` reply, shared verbatim by the
|
|
53
|
+
* local server and the relay client so the two lanes cannot drift. The reply
|
|
54
|
+
* carries names only — never a value.
|
|
55
|
+
*/
|
|
56
|
+
export declare function handleSaveEnvFrame(frame: {
|
|
57
|
+
rid?: unknown;
|
|
58
|
+
sessionId?: unknown;
|
|
59
|
+
entries?: unknown;
|
|
60
|
+
}, sessions: {
|
|
61
|
+
list(): string[];
|
|
62
|
+
cwdOf(id: string): string | undefined;
|
|
63
|
+
}, deps?: EnvSaverDeps): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.realEnvSaverDeps = exports.MAX_SAVE_ENV_ENTRIES = exports.MAX_SAVE_ENV_VALUE_BYTES = exports.SAVE_ENV_NAME_RE = void 0;
|
|
37
|
+
exports.validateEnvEntries = validateEnvEntries;
|
|
38
|
+
exports.formatDotenvValue = formatDotenvValue;
|
|
39
|
+
exports.upsertDotenv = upsertDotenv;
|
|
40
|
+
exports.saveEnvEntries = saveEnvEntries;
|
|
41
|
+
exports.handleSaveEnvFrame = handleSaveEnvFrame;
|
|
42
|
+
/**
|
|
43
|
+
* Paste a key → saved to .env (2026-10-17, owner-approved) — the WRITE half.
|
|
44
|
+
*
|
|
45
|
+
* The pane intercepts a paste that is a key, asks "Save to .env?", and on Save
|
|
46
|
+
* sends `{t:'save-env', rid, sessionId, entries:[{name,value}]}` over its own
|
|
47
|
+
* socket (local loopback, or the relay into the hosted container — same frame,
|
|
48
|
+
* same handler, both lanes, like `attach-file`). This module writes
|
|
49
|
+
* `<session cwd>/.env`:
|
|
50
|
+
*
|
|
51
|
+
* - The folder comes from the SESSION (cwdOf), never the frame. A session
|
|
52
|
+
* with no project folder is refused — never falls back to $HOME.
|
|
53
|
+
* - `.env` that git TRACKS is refused (writing a key into it would commit it).
|
|
54
|
+
* - `.env` is added to `.gitignore` first when git does not already ignore it.
|
|
55
|
+
* - NAME=value replaces an existing NAME line (no duplicates); others are kept.
|
|
56
|
+
* - File mode 0600, written via temp file + rename.
|
|
57
|
+
* - A symlinked `.env`/`.gitignore` is refused (no writes outside the root).
|
|
58
|
+
*
|
|
59
|
+
* Values are NEVER logged, echoed in a reply, or kept in memory past the write.
|
|
60
|
+
*/
|
|
61
|
+
const fs = __importStar(require("fs"));
|
|
62
|
+
const path = __importStar(require("path"));
|
|
63
|
+
const crypto = __importStar(require("crypto"));
|
|
64
|
+
const child_process_1 = require("child_process");
|
|
65
|
+
const repoEnv_1 = require("./repoEnv");
|
|
66
|
+
exports.SAVE_ENV_NAME_RE = /^[A-Z_][A-Z0-9_]{0,63}$/;
|
|
67
|
+
exports.MAX_SAVE_ENV_VALUE_BYTES = 4096;
|
|
68
|
+
exports.MAX_SAVE_ENV_ENTRIES = 32;
|
|
69
|
+
/** Validate the frame's entries. Error text is user-facing and never quotes a value. */
|
|
70
|
+
function validateEnvEntries(raw) {
|
|
71
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
72
|
+
return { ok: false, error: 'Nothing to save' };
|
|
73
|
+
if (raw.length > exports.MAX_SAVE_ENV_ENTRIES)
|
|
74
|
+
return { ok: false, error: 'Too many keys' };
|
|
75
|
+
const byName = new Map();
|
|
76
|
+
for (const e of raw) {
|
|
77
|
+
const name = e?.name;
|
|
78
|
+
const value = e?.value;
|
|
79
|
+
if (typeof name !== 'string' || !exports.SAVE_ENV_NAME_RE.test(name))
|
|
80
|
+
return { ok: false, error: 'Invalid name' };
|
|
81
|
+
if ((0, repoEnv_1.isHiveOpOwnedEnvName)(name))
|
|
82
|
+
return { ok: false, error: `${name} is reserved` };
|
|
83
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
84
|
+
return { ok: false, error: `${name}: empty value` };
|
|
85
|
+
if (/[\r\n\0]/.test(value))
|
|
86
|
+
return { ok: false, error: `${name}: value has a line break` };
|
|
87
|
+
if (Buffer.byteLength(value, 'utf8') > exports.MAX_SAVE_ENV_VALUE_BYTES)
|
|
88
|
+
return { ok: false, error: `${name}: value too long` };
|
|
89
|
+
byName.set(name, value);
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, entries: [...byName].map(([name, value]) => ({ name, value })) };
|
|
92
|
+
}
|
|
93
|
+
/** Serialize one value so repoEnv.parseDotenv reads back exactly the same string. */
|
|
94
|
+
function formatDotenvValue(value) {
|
|
95
|
+
if (/^[A-Za-z0-9_\-./:+=@,~%^]+$/.test(value))
|
|
96
|
+
return value;
|
|
97
|
+
if (!value.includes("'"))
|
|
98
|
+
return `'${value}'`;
|
|
99
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
100
|
+
}
|
|
101
|
+
function lineName(line) {
|
|
102
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
|
|
103
|
+
return m ? m[1] : null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Upsert entries into dotenv text: the first line for a NAME is replaced in
|
|
107
|
+
* place, later duplicates of it are dropped, new names are appended. Every
|
|
108
|
+
* other line (comments, unrelated keys) is preserved byte for byte.
|
|
109
|
+
*/
|
|
110
|
+
function upsertDotenv(existing, entries) {
|
|
111
|
+
const pending = new Map(entries.map((e) => [e.name, e.value]));
|
|
112
|
+
const written = new Set();
|
|
113
|
+
const lines = existing.length > 0 ? existing.split('\n') : [];
|
|
114
|
+
// A trailing newline yields one empty final element — keep it out of the loop.
|
|
115
|
+
const hadTrailingNewline = existing.endsWith('\n');
|
|
116
|
+
if (hadTrailingNewline)
|
|
117
|
+
lines.pop();
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
const name = lineName(line);
|
|
121
|
+
if (name && pending.has(name)) {
|
|
122
|
+
if (written.has(name))
|
|
123
|
+
continue; // a duplicate — never keep two
|
|
124
|
+
out.push(`${name}=${formatDotenvValue(pending.get(name))}`);
|
|
125
|
+
written.add(name);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
out.push(line);
|
|
129
|
+
}
|
|
130
|
+
for (const [name, value] of pending) {
|
|
131
|
+
if (!written.has(name))
|
|
132
|
+
out.push(`${name}=${formatDotenvValue(value)}`);
|
|
133
|
+
}
|
|
134
|
+
return out.join('\n') + '\n';
|
|
135
|
+
}
|
|
136
|
+
exports.realEnvSaverDeps = {
|
|
137
|
+
realpath: (p) => fs.realpathSync(p),
|
|
138
|
+
lstat: (p) => {
|
|
139
|
+
try {
|
|
140
|
+
return fs.lstatSync(p);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
readFile: (p) => fs.readFileSync(p, 'utf8'),
|
|
147
|
+
writeFileAtomic0600: (p, data) => {
|
|
148
|
+
const tmp = `${p}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
149
|
+
fs.writeFileSync(tmp, data, { mode: 0o600 });
|
|
150
|
+
try {
|
|
151
|
+
fs.chmodSync(tmp, 0o600);
|
|
152
|
+
fs.renameSync(tmp, p);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
try {
|
|
156
|
+
fs.unlinkSync(tmp);
|
|
157
|
+
}
|
|
158
|
+
catch { /* best effort */ }
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
fs.chmodSync(p, 0o600);
|
|
162
|
+
},
|
|
163
|
+
appendFile: (p, data) => fs.appendFileSync(p, data),
|
|
164
|
+
git: (args, cwd) => new Promise((resolve) => {
|
|
165
|
+
(0, child_process_1.execFile)('git', args, { cwd, timeout: 10_000, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } }, (err) => {
|
|
166
|
+
if (!err)
|
|
167
|
+
return resolve(0);
|
|
168
|
+
const code = err.code;
|
|
169
|
+
resolve(typeof code === 'number' ? code : 128);
|
|
170
|
+
});
|
|
171
|
+
}),
|
|
172
|
+
};
|
|
173
|
+
/** Write entries into `<root>/.env`. `root` is the session's cwd. */
|
|
174
|
+
async function saveEnvEntries(root, entries, deps = exports.realEnvSaverDeps) {
|
|
175
|
+
let realRoot;
|
|
176
|
+
try {
|
|
177
|
+
realRoot = deps.realpath(root);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return { ok: false, error: 'Project folder not found' };
|
|
181
|
+
}
|
|
182
|
+
const rootStat = deps.lstat(realRoot);
|
|
183
|
+
if (!rootStat || !rootStat.isDirectory())
|
|
184
|
+
return { ok: false, error: 'Project folder not found' };
|
|
185
|
+
const envPath = path.join(realRoot, repoEnv_1.DOTENV_FILENAME);
|
|
186
|
+
const ignorePath = path.join(realRoot, '.gitignore');
|
|
187
|
+
// Containment: both files are direct children of the resolved root, and
|
|
188
|
+
// neither may be a symlink (which could point anywhere).
|
|
189
|
+
if (path.dirname(envPath) !== realRoot || path.dirname(ignorePath) !== realRoot) {
|
|
190
|
+
return { ok: false, error: 'Bad project folder' };
|
|
191
|
+
}
|
|
192
|
+
const envStat = deps.lstat(envPath);
|
|
193
|
+
if (envStat && (envStat.isSymbolicLink() || !envStat.isFile()))
|
|
194
|
+
return { ok: false, error: '.env is not a plain file' };
|
|
195
|
+
const ignoreStat = deps.lstat(ignorePath);
|
|
196
|
+
if (ignoreStat && (ignoreStat.isSymbolicLink() || !ignoreStat.isFile()))
|
|
197
|
+
return { ok: false, error: '.gitignore is not a plain file' };
|
|
198
|
+
// Tracked .env → refuse: the key would be committed with the next commit.
|
|
199
|
+
const tracked = await deps.git(['ls-files', '--error-unmatch', '--', repoEnv_1.DOTENV_FILENAME], realRoot);
|
|
200
|
+
if (tracked === 0)
|
|
201
|
+
return { ok: false, error: '.env is tracked by git' };
|
|
202
|
+
// Ignore it BEFORE the key lands, so it is never briefly committable.
|
|
203
|
+
// check-ignore: 0 = ignored, 1 = not ignored, 128 = not a repo / error.
|
|
204
|
+
let gitignoreAdded = false;
|
|
205
|
+
try {
|
|
206
|
+
const ignored = await deps.git(['check-ignore', '-q', '--', repoEnv_1.DOTENV_FILENAME], realRoot);
|
|
207
|
+
if (ignored !== 0) {
|
|
208
|
+
const current = ignoreStat ? deps.readFile(ignorePath) : '';
|
|
209
|
+
const sep = current.length > 0 && !current.endsWith('\n') ? '\n' : '';
|
|
210
|
+
deps.appendFile(ignorePath, `${sep}${repoEnv_1.DOTENV_FILENAME}\n`);
|
|
211
|
+
gitignoreAdded = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return { ok: false, error: "Couldn't update .gitignore" };
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const existing = envStat ? deps.readFile(envPath) : '';
|
|
219
|
+
deps.writeFileAtomic0600(envPath, upsertDotenv(existing, entries));
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return { ok: false, error: "Couldn't write .env" };
|
|
223
|
+
}
|
|
224
|
+
return { ok: true, names: entries.map((e) => e.name), gitignoreAdded };
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The whole `save-env` frame → `save-env-result` reply, shared verbatim by the
|
|
228
|
+
* local server and the relay client so the two lanes cannot drift. The reply
|
|
229
|
+
* carries names only — never a value.
|
|
230
|
+
*/
|
|
231
|
+
async function handleSaveEnvFrame(frame, sessions, deps = exports.realEnvSaverDeps) {
|
|
232
|
+
const head = {
|
|
233
|
+
t: 'save-env-result',
|
|
234
|
+
...(typeof frame.rid === 'string' ? { rid: frame.rid } : {}),
|
|
235
|
+
...(typeof frame.sessionId === 'string' ? { sessionId: frame.sessionId } : {}),
|
|
236
|
+
};
|
|
237
|
+
const sessionId = frame.sessionId;
|
|
238
|
+
if (typeof sessionId !== 'string' || !sessions.list().includes(sessionId)) {
|
|
239
|
+
return { ...head, ok: false, error: 'Terminal session not found' };
|
|
240
|
+
}
|
|
241
|
+
const root = sessions.cwdOf(sessionId);
|
|
242
|
+
if (!root)
|
|
243
|
+
return { ...head, ok: false, error: 'No project folder' };
|
|
244
|
+
const valid = validateEnvEntries(frame.entries);
|
|
245
|
+
if (!valid.ok)
|
|
246
|
+
return { ...head, ok: false, error: valid.error };
|
|
247
|
+
try {
|
|
248
|
+
const result = await saveEnvEntries(root, valid.entries, deps);
|
|
249
|
+
return result.ok
|
|
250
|
+
? { ...head, ok: true, names: result.names, gitignoreAdded: result.gitignoreAdded }
|
|
251
|
+
: { ...head, ok: false, error: result.error };
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return { ...head, ok: false, error: "Couldn't save key" };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -81,6 +81,11 @@ function hookCommand(hooksDir, file) {
|
|
|
81
81
|
function buildHiveOpSettingsContent(hooksDir) {
|
|
82
82
|
const obj = {
|
|
83
83
|
hooks: {
|
|
84
|
+
// Secret guard (2026-10-17): blocks a Bash command carrying a literal
|
|
85
|
+
// secret and tells the agent to reference it as $NAME instead.
|
|
86
|
+
PreToolUse: [
|
|
87
|
+
{ matcher: 'Bash', hooks: [{ type: 'command', command: hookCommand(hooksDir, 'pre-tool-use.js') }] },
|
|
88
|
+
],
|
|
84
89
|
PostToolUse: [
|
|
85
90
|
{
|
|
86
91
|
matcher: 'Read|Edit|Write|Update|Create|MultiEdit|NotebookEdit|Bash',
|
|
@@ -6,6 +6,7 @@ exports.firstMeaningfulLine = firstMeaningfulLine;
|
|
|
6
6
|
exports.retryArgsFor = retryArgsFor;
|
|
7
7
|
exports.friendlyStartFailure = friendlyStartFailure;
|
|
8
8
|
exports.superviseLaunch = superviseLaunch;
|
|
9
|
+
const redact_1 = require("../secrets/redact");
|
|
9
10
|
/** How long after launch an exit still counts as "couldn't start". */
|
|
10
11
|
exports.DEFAULT_LAUNCH_WINDOW_MS = 10_000;
|
|
11
12
|
/** Cap on how much early output is kept for matching/logging. */
|
|
@@ -127,7 +128,9 @@ function superviseLaunch(factory, spawnOpts, opts = {}) {
|
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
130
|
const raw = stripAnsi(captured).trim();
|
|
130
|
-
|
|
131
|
+
// The captured startup output can hold anything the shell printed, so it
|
|
132
|
+
// is masked before it reaches bridge.log (secret masking, 2026-10-17).
|
|
133
|
+
log((0, redact_1.redact)(`[bridge] claude exited at startup (code ${code}, args: ${args.join(' ')}): ${raw || '(no output)'}`));
|
|
131
134
|
const nextArgs = retried ? null : retryArgsFor(args, captured);
|
|
132
135
|
if (nextArgs) {
|
|
133
136
|
retried = true;
|
|
@@ -97,14 +97,18 @@ function writeLocalContext(input, deps) {
|
|
|
97
97
|
const browser = (0, workGraphPointer_1.browserSection)(input);
|
|
98
98
|
if (browser)
|
|
99
99
|
pointers.push(browser);
|
|
100
|
+
const keys = (0, workGraphPointer_1.keysSection)(input);
|
|
101
|
+
if (keys)
|
|
102
|
+
pointers.push(keys);
|
|
103
|
+
const sections = (0, workGraphPointer_1.withSecretsSection)(pointers);
|
|
100
104
|
// wmr-07 — the whole local file is bridge-owned, so "preserve the pushed
|
|
101
105
|
// content" means keeping what is already on disk below the pointer fence.
|
|
102
106
|
const existingBody = existed ? deps.readFile(contextPath) : '';
|
|
103
107
|
const fileContent = input.pointersOnly
|
|
104
108
|
? splitPointers(existingBody).rest
|
|
105
109
|
: (input.content ?? '').trim();
|
|
106
|
-
const block =
|
|
107
|
-
? `${contextWriter_1.POINTERS_START_MARKER}\n${
|
|
110
|
+
const block = sections.length > 0
|
|
111
|
+
? `${contextWriter_1.POINTERS_START_MARKER}\n${sections.join('\n\n')}\n${contextWriter_1.POINTERS_END_MARKER}`
|
|
108
112
|
: '';
|
|
109
113
|
const content = [block, fileContent].filter((part) => part.length > 0).join('\n\n');
|
|
110
114
|
const isEmpty = content.trim().length === 0;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is attaching a terminal pane — so last-attach-wins can tell a real
|
|
3
|
+
* second viewer from the same pane coming back.
|
|
4
|
+
*
|
|
5
|
+
* Prod 2026-10-17: panes showed "Session attached elsewhere" with nobody
|
|
6
|
+
* else attached. On this (loopback) lane a second mount of the same agent in
|
|
7
|
+
* one page, or a pane re-attaching on its own socket, got the same takeover a
|
|
8
|
+
* second device does. The arena sends three ids with each open/attach (the
|
|
9
|
+
* backend relay's pane-client.ts is the hosted twin of this file):
|
|
10
|
+
*
|
|
11
|
+
* browser — one per browser profile (localStorage)
|
|
12
|
+
* tab — one per page load
|
|
13
|
+
* mount — one per terminal pane instance, stable across its reconnects
|
|
14
|
+
*
|
|
15
|
+
* All optional: an old client sends none and gets today's plain takeover.
|
|
16
|
+
*/
|
|
17
|
+
export interface PaneClient {
|
|
18
|
+
browser?: string;
|
|
19
|
+
tab?: string;
|
|
20
|
+
mount?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Where the NEW attachment is, from the superseded one's point of view.
|
|
23
|
+
* 'self' = the same pane instance redialing: evict the ghost silently. */
|
|
24
|
+
export type TakeoverWhere = 'self' | 'page' | 'tab' | 'device';
|
|
25
|
+
/** Accept only short opaque ids; anything else is treated as absent. */
|
|
26
|
+
export declare function parsePaneClient(raw: unknown): PaneClient | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Compare the superseded pane's identity with the new one's. Undefined when
|
|
29
|
+
* either side did not say who it is (old client) — the caller keeps the
|
|
30
|
+
* plain takeover it always sent.
|
|
31
|
+
*/
|
|
32
|
+
export declare function takeoverWhere(prev: PaneClient | undefined, next: PaneClient | undefined): TakeoverWhere | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parsePaneClient = parsePaneClient;
|
|
4
|
+
exports.takeoverWhere = takeoverWhere;
|
|
5
|
+
const ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
6
|
+
/** Accept only short opaque ids; anything else is treated as absent. */
|
|
7
|
+
function parsePaneClient(raw) {
|
|
8
|
+
if (!raw || typeof raw !== 'object')
|
|
9
|
+
return undefined;
|
|
10
|
+
const r = raw;
|
|
11
|
+
const pick = (v) => (typeof v === 'string' && ID.test(v) ? v : undefined);
|
|
12
|
+
const out = { browser: pick(r.browser), tab: pick(r.tab), mount: pick(r.mount) };
|
|
13
|
+
return out.browser || out.tab || out.mount ? out : undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Compare the superseded pane's identity with the new one's. Undefined when
|
|
17
|
+
* either side did not say who it is (old client) — the caller keeps the
|
|
18
|
+
* plain takeover it always sent.
|
|
19
|
+
*/
|
|
20
|
+
function takeoverWhere(prev, next) {
|
|
21
|
+
if (!prev || !next)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (prev.mount && next.mount && prev.mount === next.mount)
|
|
24
|
+
return 'self';
|
|
25
|
+
if (prev.tab && next.tab && prev.tab === next.tab)
|
|
26
|
+
return 'page';
|
|
27
|
+
if (prev.browser && next.browser)
|
|
28
|
+
return prev.browser === next.browser ? 'tab' : 'device';
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ProcEntry {
|
|
2
|
+
ppid: number;
|
|
3
|
+
/** utime + stime + cutime + cstime, in clock ticks. */
|
|
4
|
+
ticks: number;
|
|
5
|
+
}
|
|
6
|
+
/** Parse one /proc/<pid>/stat line. The command name sits in parentheses and
|
|
7
|
+
* may itself contain spaces or parentheses, so split after the LAST ')'. */
|
|
8
|
+
export declare function parseProcStat(line: string): ProcEntry | null;
|
|
9
|
+
export declare function readProcTable(procRoot?: string): Map<number, ProcEntry>;
|
|
10
|
+
/** Total ticks of `root` and every live descendant. */
|
|
11
|
+
export declare function treeTicks(table: Map<number, ProcEntry>, root: number): number;
|
|
12
|
+
export interface CpuBusyTrackerOptions {
|
|
13
|
+
readTable?: () => Map<number, ProcEntry>;
|
|
14
|
+
now?: () => number;
|
|
15
|
+
/** Clock ticks per second (Linux USER_HZ — 100 on every mainstream kernel). */
|
|
16
|
+
clkTck?: number;
|
|
17
|
+
/** Busy at or above this share of one core, averaged since the last sample. */
|
|
18
|
+
thresholdPct?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface CpuBusyTracker {
|
|
21
|
+
/** Session ids whose tree used ≥ threshold CPU since the previous sample.
|
|
22
|
+
* A session seen for the first time only sets its baseline. */
|
|
23
|
+
sample(sessions: Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
pid: number | undefined;
|
|
26
|
+
}>): string[];
|
|
27
|
+
}
|
|
28
|
+
export declare const DEFAULT_BUSY_CPU_PCT = 20;
|
|
29
|
+
export declare function createCpuBusyTracker(opts?: CpuBusyTrackerOptions): CpuBusyTracker | null;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_BUSY_CPU_PCT = void 0;
|
|
37
|
+
exports.parseProcStat = parseProcStat;
|
|
38
|
+
exports.readProcTable = readProcTable;
|
|
39
|
+
exports.treeTicks = treeTicks;
|
|
40
|
+
exports.createCpuBusyTracker = createCpuBusyTracker;
|
|
41
|
+
/**
|
|
42
|
+
* Activity-based sleep — which PTY sessions are doing work nobody can see?
|
|
43
|
+
*
|
|
44
|
+
* The backend suspends an idle box (HT_ACTIVITY_IDLE=1). Output and keystrokes
|
|
45
|
+
* already count as activity; this covers the silent rest: a build, a test run
|
|
46
|
+
* or an install a backgrounded shell runs while the agent sits at its prompt.
|
|
47
|
+
* We measure CPU, not "is a child process alive", on purpose: an idle dev
|
|
48
|
+
* server is alive forever and must not hold a box awake, but one actually
|
|
49
|
+
* serving requests burns CPU and does.
|
|
50
|
+
*
|
|
51
|
+
* Linux only (reads /proc). Elsewhere `createCpuBusyTracker` returns null and
|
|
52
|
+
* the `sessions` frame carries no `busy` field, which the backend reads as
|
|
53
|
+
* unknown — never as busy.
|
|
54
|
+
*
|
|
55
|
+
* A session's cost is its whole process tree: utime+stime of every live
|
|
56
|
+
* process under the PTY, plus cutime+cstime, which already holds the CPU of
|
|
57
|
+
* children that have exited and been reaped (an install's short-lived compiler
|
|
58
|
+
* runs). A reaped intermediate parent takes its children's time with it; the
|
|
59
|
+
* delta is clamped at 0, so that can only under-report.
|
|
60
|
+
*/
|
|
61
|
+
const fs = __importStar(require("fs"));
|
|
62
|
+
const path = __importStar(require("path"));
|
|
63
|
+
/** Parse one /proc/<pid>/stat line. The command name sits in parentheses and
|
|
64
|
+
* may itself contain spaces or parentheses, so split after the LAST ')'. */
|
|
65
|
+
function parseProcStat(line) {
|
|
66
|
+
const close = line.lastIndexOf(')');
|
|
67
|
+
if (close < 0)
|
|
68
|
+
return null;
|
|
69
|
+
const f = line.slice(close + 2).trim().split(/\s+/);
|
|
70
|
+
// f[0]=state f[1]=ppid … f[11]=utime f[12]=stime f[13]=cutime f[14]=cstime
|
|
71
|
+
if (f.length < 15)
|
|
72
|
+
return null;
|
|
73
|
+
const nums = [f[1], f[11], f[12], f[13], f[14]].map(Number);
|
|
74
|
+
if (nums.some((n) => !Number.isFinite(n)))
|
|
75
|
+
return null;
|
|
76
|
+
return { ppid: nums[0], ticks: nums[1] + nums[2] + nums[3] + nums[4] };
|
|
77
|
+
}
|
|
78
|
+
function readProcTable(procRoot = '/proc') {
|
|
79
|
+
const table = new Map();
|
|
80
|
+
let names;
|
|
81
|
+
try {
|
|
82
|
+
names = fs.readdirSync(procRoot);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return table;
|
|
86
|
+
}
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
if (!/^\d+$/.test(name))
|
|
89
|
+
continue;
|
|
90
|
+
try {
|
|
91
|
+
const e = parseProcStat(fs.readFileSync(path.join(procRoot, name, 'stat'), 'utf8'));
|
|
92
|
+
if (e)
|
|
93
|
+
table.set(Number(name), e);
|
|
94
|
+
}
|
|
95
|
+
catch { /* exited between readdir and read */ }
|
|
96
|
+
}
|
|
97
|
+
return table;
|
|
98
|
+
}
|
|
99
|
+
/** Total ticks of `root` and every live descendant. */
|
|
100
|
+
function treeTicks(table, root) {
|
|
101
|
+
const children = new Map();
|
|
102
|
+
for (const [pid, e] of table) {
|
|
103
|
+
const list = children.get(e.ppid);
|
|
104
|
+
if (list)
|
|
105
|
+
list.push(pid);
|
|
106
|
+
else
|
|
107
|
+
children.set(e.ppid, [pid]);
|
|
108
|
+
}
|
|
109
|
+
let total = 0;
|
|
110
|
+
const stack = [root];
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
while (stack.length) {
|
|
113
|
+
const pid = stack.pop();
|
|
114
|
+
if (seen.has(pid))
|
|
115
|
+
continue;
|
|
116
|
+
seen.add(pid);
|
|
117
|
+
const e = table.get(pid);
|
|
118
|
+
if (e)
|
|
119
|
+
total += e.ticks;
|
|
120
|
+
for (const c of children.get(pid) ?? [])
|
|
121
|
+
stack.push(c);
|
|
122
|
+
}
|
|
123
|
+
return total;
|
|
124
|
+
}
|
|
125
|
+
exports.DEFAULT_BUSY_CPU_PCT = 20;
|
|
126
|
+
function createCpuBusyTracker(opts = {}) {
|
|
127
|
+
const readTable = opts.readTable ?? (process.platform === 'linux' && fs.existsSync('/proc/self/stat') ? () => readProcTable() : null);
|
|
128
|
+
if (!readTable)
|
|
129
|
+
return null;
|
|
130
|
+
const now = opts.now ?? Date.now;
|
|
131
|
+
const clkTck = opts.clkTck ?? 100;
|
|
132
|
+
const thresholdPct = opts.thresholdPct ?? (Number(process.env.HIVEOP_BUSY_CPU_PCT) || exports.DEFAULT_BUSY_CPU_PCT);
|
|
133
|
+
const last = new Map();
|
|
134
|
+
return {
|
|
135
|
+
sample(sessions) {
|
|
136
|
+
const table = readTable();
|
|
137
|
+
const at = now();
|
|
138
|
+
const busy = [];
|
|
139
|
+
const live = new Set();
|
|
140
|
+
for (const { id, pid } of sessions) {
|
|
141
|
+
if (typeof pid !== 'number' || !table.has(pid))
|
|
142
|
+
continue;
|
|
143
|
+
live.add(id);
|
|
144
|
+
const ticks = treeTicks(table, pid);
|
|
145
|
+
const prev = last.get(id);
|
|
146
|
+
last.set(id, { pid, ticks, at });
|
|
147
|
+
if (!prev || prev.pid !== pid || at <= prev.at)
|
|
148
|
+
continue;
|
|
149
|
+
const cpuSeconds = Math.max(0, ticks - prev.ticks) / clkTck;
|
|
150
|
+
const pct = (cpuSeconds / ((at - prev.at) / 1000)) * 100;
|
|
151
|
+
if (pct >= thresholdPct)
|
|
152
|
+
busy.push(id);
|
|
153
|
+
}
|
|
154
|
+
for (const id of [...last.keys()])
|
|
155
|
+
if (!live.has(id))
|
|
156
|
+
last.delete(id);
|
|
157
|
+
return busy;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
package/dist/bridge/realPty.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.realPtyFactory = realPtyFactory;
|
|
|
41
41
|
*/
|
|
42
42
|
const os = __importStar(require("os"));
|
|
43
43
|
const envSanitize_1 = require("./envSanitize");
|
|
44
|
+
const repoEnv_1 = require("./repoEnv");
|
|
44
45
|
const fs = __importStar(require("fs"));
|
|
45
46
|
const path = __importStar(require("path"));
|
|
46
47
|
/**
|
|
@@ -86,7 +87,10 @@ function realPtyFactory() {
|
|
|
86
87
|
// Sanitized: the launcher's Claude-session markers must not leak into
|
|
87
88
|
// the agent (see envSanitize.ts — a bridge started from inside a Claude
|
|
88
89
|
// Code session was silently disabling transcript saving in every agent).
|
|
89
|
-
|
|
90
|
+
// Paste a key → .env: the session folder's `.env` is loaded too —
|
|
91
|
+
// above the inherited env, below the session's own (WENRWA_* etc.),
|
|
92
|
+
// with reserved names never taken from the file (repoEnv.ts).
|
|
93
|
+
env: (0, repoEnv_1.mergeSpawnEnv)((0, envSanitize_1.sanitizeInheritedEnv)(process.env), (0, repoEnv_1.loadRepoEnv)(cwd), env),
|
|
90
94
|
});
|
|
91
95
|
return {
|
|
92
96
|
write: (data) => proc.write(data),
|
|
@@ -94,6 +98,7 @@ function realPtyFactory() {
|
|
|
94
98
|
kill: () => proc.kill(),
|
|
95
99
|
onData: (cb) => proc.onData(cb),
|
|
96
100
|
onExit: (cb) => proc.onExit(({ exitCode }) => cb(exitCode)),
|
|
101
|
+
pid: typeof proc.pid === 'number' ? proc.pid : undefined,
|
|
97
102
|
};
|
|
98
103
|
},
|
|
99
104
|
};
|