@volter/supercode-terminal 0.2.72 → 0.2.74
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/index.d.ts +2 -1
- package/index.mjs +22 -10
- package/package.json +2 -1
- package/substrate/harness-env.d.ts +12 -0
- package/substrate/harness-env.js +29 -0
- package/substrate/harness-markers.json +23 -0
- package/substrate/internal/exec.js +2 -1
- package/substrate/tmux-stream.js +2 -1
- package/substrate/tmux.js +5 -2
package/index.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export class TmuxTerminalHost {
|
|
|
58
58
|
env?: Record<string, string>;
|
|
59
59
|
name?: string;
|
|
60
60
|
}): Promise<TerminalSession>;
|
|
61
|
-
capture(sessionId: string, options?: { lines?: number; preserveEscapes?: boolean }): string
|
|
61
|
+
capture(sessionId: string, options?: { lines?: number; preserveEscapes?: boolean }): Promise<string>;
|
|
62
62
|
sendInput(
|
|
63
63
|
sessionId: string,
|
|
64
64
|
data: string,
|
|
@@ -258,6 +258,7 @@ export function composerReadyForInput(kind: string, screen: string | undefined):
|
|
|
258
258
|
export function composerHoldsDraft(kind: string, screen: string | undefined): boolean | null;
|
|
259
259
|
|
|
260
260
|
export { classifyAttentionSignal, observeLifecycle, gatherLifecycleObservation, createLifecycleStore, snapshotProcessTree, snapshotProcessTreeAsync } from './substrate/session-lifecycle.js';
|
|
261
|
+
export { HARNESS_MARKERS, HARNESS_CHILD_SESSION_MARKERS, isHarnessMarker, withoutHarnessMarkers, dropOwnHarnessMarkers } from './substrate/harness-env.js';
|
|
261
262
|
export type {
|
|
262
263
|
LifecycleSnapshot,
|
|
263
264
|
LifecycleStore,
|
package/index.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
import { createServer as defaultCreateServer } from 'node:http';
|
|
10
10
|
|
|
11
11
|
import { NativeTerminalSessionHost } from './substrate/native-host.js';
|
|
12
|
+
import { withoutHarnessMarkers } from './substrate/harness-env.js';
|
|
12
13
|
import * as defaultTmux from './substrate/tmux.js';
|
|
13
14
|
import { openLocalTerminal as defaultOpenLocalTerminal } from './substrate/local-terminal.js';
|
|
14
15
|
|
|
@@ -293,14 +294,24 @@ export class TmuxTerminalHost {
|
|
|
293
294
|
return created;
|
|
294
295
|
}
|
|
295
296
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
297
|
+
// Asynchronous, and one tmux process: the daemon reads every pane's screen each time it judges attention, and a
|
|
298
|
+
// synchronous read (a session listing and a capture per pane, each a spawnSync) held its event loop for all of them
|
|
299
|
+
// back to back, so a local client's hello waited behind the whole pass. A session that is gone is told from the
|
|
300
|
+
// failed capture, by the same listing as before.
|
|
301
|
+
async capture(sessionId, options = {}) {
|
|
302
|
+
const entry = this.#catalog.get(sessionId);
|
|
303
|
+
if (!entry) throw new TerminalHostError('session_not_found', 'The tmux session no longer exists.');
|
|
304
|
+
try {
|
|
305
|
+
return await this.#tmux.capturePaneAsync({
|
|
306
|
+
lines: options.lines,
|
|
307
|
+
preserveEscapes: options.preserveEscapes === true,
|
|
308
|
+
socket: this.#socket,
|
|
309
|
+
target: `${entry.name}:0.0`,
|
|
310
|
+
});
|
|
311
|
+
} catch (error) {
|
|
312
|
+
this.#entry(sessionId);
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
304
315
|
}
|
|
305
316
|
|
|
306
317
|
async sendInput(sessionId, data, options = {}) {
|
|
@@ -672,7 +683,7 @@ export class PtyTerminalHost {
|
|
|
672
683
|
// a half-installed package): the pane server answers this code by ending itself, and the next one loads afresh.
|
|
673
684
|
throw new TerminalHostError('pane_runtime_missing', `The pane runtime could not load: ${errorMessage(error)}`);
|
|
674
685
|
}
|
|
675
|
-
const env = { ...process.env, ...(input.env ?? {}) };
|
|
686
|
+
const env = { ...withoutHarnessMarkers(process.env), ...(input.env ?? {}) };
|
|
676
687
|
const { file, args } = windowsLaunch(input.command.program, input.command.arguments ?? [], env);
|
|
677
688
|
// The watchdog is running before the pane is, and the pane is on its list the moment it exists.
|
|
678
689
|
this.#startWatchdog();
|
|
@@ -1230,7 +1241,7 @@ export class PaneServerHost {
|
|
|
1230
1241
|
} catch (error) {
|
|
1231
1242
|
// No server on the pipe yet: start one, detached from this daemon, and try again as it comes up.
|
|
1232
1243
|
if (attempt === 0 && (error.code === 'ENOENT' || error.code === 'ECONNREFUSED')) {
|
|
1233
|
-
const child = spawn(process.execPath, [this.#script(), this.#pipe], { detached: true, stdio: 'ignore', windowsHide: true, env: process.env });
|
|
1244
|
+
const child = spawn(process.execPath, [this.#script(), this.#pipe], { detached: true, stdio: 'ignore', windowsHide: true, env: withoutHarnessMarkers(process.env) });
|
|
1234
1245
|
child.on('error', () => {});
|
|
1235
1246
|
child.unref();
|
|
1236
1247
|
}
|
|
@@ -2164,3 +2175,4 @@ function randomToken() {
|
|
|
2164
2175
|
// every surface that triages panes shares one precision rather than each
|
|
2165
2176
|
// inventing its own patterns.
|
|
2166
2177
|
export { classifyAttentionSignal, observeLifecycle, gatherLifecycleObservation, createLifecycleStore, snapshotProcessTree, snapshotProcessTreeAsync } from './substrate/session-lifecycle.js';
|
|
2178
|
+
export { HARNESS_MARKERS, HARNESS_CHILD_SESSION_MARKERS, isHarnessMarker, withoutHarnessMarkers, dropOwnHarnessMarkers } from './substrate/harness-env.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-terminal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.74",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Optional leak-safe terminal and tmux host adapter for Volter Harness frontends",
|
|
6
6
|
"exports": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"types": "./react/stream.d.ts",
|
|
30
30
|
"import": "./react/stream.mjs"
|
|
31
31
|
},
|
|
32
|
+
"./harness-markers.json": "./substrate/harness-markers.json",
|
|
32
33
|
"./docker-exec": {
|
|
33
34
|
"types": "./substrate/docker-exec.d.ts",
|
|
34
35
|
"import": "./substrate/docker-exec.js"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** What a harness takes itself for a nested session by. Each is read for truth, so an empty value is no mark. */
|
|
2
|
+
export declare const HARNESS_CHILD_SESSION_MARKERS: readonly string[];
|
|
3
|
+
/** Every mark: the child-session ones, and what names the session a command ran for. */
|
|
4
|
+
export declare const HARNESS_MARKERS: readonly string[];
|
|
5
|
+
export declare function isHarnessMarker(name: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* The environment a process supercode starts gets: `env` without the calling harness's own marks. `keep` names what
|
|
8
|
+
* the launch sets on purpose.
|
|
9
|
+
*/
|
|
10
|
+
export declare function withoutHarnessMarkers<T extends Record<string, string | undefined>>(env: T, keep?: Iterable<string>): T;
|
|
11
|
+
/** Drops the marks from this process's own environment, so nothing it starts later can inherit them. */
|
|
12
|
+
export declare function dropOwnHarnessMarkers(env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
// A coding harness marks what its tools run: Claude Code exports CLAUDECODE, CLAUDE_CODE_CHILD_SESSION and the rest
|
|
3
|
+
// to every command of a session, Codex its thread. A process supercode starts that outlives the command (a terminal
|
|
4
|
+
// app, an attach client, a pane, the daemon) is not that session's child: carrying the marks, every shell a person
|
|
5
|
+
// opens in that terminal starts harnesses that take themselves for a nested session (Claude Code then saves no
|
|
6
|
+
// transcript). The names are harness-markers.json beside the built module, the one list; the CLI reads the same file
|
|
7
|
+
// (crates/cli/src/harness_env.rs).
|
|
8
|
+
const listed = JSON.parse(readFileSync(new URL("./harness-markers.json", import.meta.url), "utf8"));
|
|
9
|
+
/** What a harness takes itself for a nested session by. Each is read for truth, so an empty value is no mark. */
|
|
10
|
+
export const HARNESS_CHILD_SESSION_MARKERS = Object.freeze([...listed.child_session]);
|
|
11
|
+
/** Every mark: the child-session ones, and what names the session a command ran for. */
|
|
12
|
+
export const HARNESS_MARKERS = Object.freeze([...listed.child_session, ...listed.session]);
|
|
13
|
+
const markers = new Set(HARNESS_MARKERS);
|
|
14
|
+
export function isHarnessMarker(name) {
|
|
15
|
+
return markers.has(name);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The environment a process supercode starts gets: `env` without the calling harness's own marks. `keep` names what
|
|
19
|
+
* the launch sets on purpose.
|
|
20
|
+
*/
|
|
21
|
+
export function withoutHarnessMarkers(env, keep = []) {
|
|
22
|
+
const kept = new Set(keep);
|
|
23
|
+
return Object.fromEntries(Object.entries(env).filter(([name]) => !markers.has(name) || kept.has(name)));
|
|
24
|
+
}
|
|
25
|
+
/** Drops the marks from this process's own environment, so nothing it starts later can inherit them. */
|
|
26
|
+
export function dropOwnHarnessMarkers(env = process.env) {
|
|
27
|
+
for (const name of HARNESS_MARKERS)
|
|
28
|
+
delete env[name];
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"child_session": [
|
|
3
|
+
"CLAUDECODE",
|
|
4
|
+
"CLAUDE_CODE_CHILD_SESSION"
|
|
5
|
+
],
|
|
6
|
+
"session": [
|
|
7
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
8
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
9
|
+
"CLAUDE_CODE_SESSION_ATTENDED",
|
|
10
|
+
"CLAUDE_CODE_EXECPATH",
|
|
11
|
+
"CLAUDE_CODE_MESSAGING_SOCKET",
|
|
12
|
+
"CLAUDE_CODE_MESSAGING_TOKEN",
|
|
13
|
+
"CLAUDE_CODE_INVOKED_SKILLS",
|
|
14
|
+
"CLAUDE_PID",
|
|
15
|
+
"CLAUDE_EFFORT",
|
|
16
|
+
"AI_AGENT",
|
|
17
|
+
"CODEX_THREAD_ID",
|
|
18
|
+
"CODEX_SESSION_ID",
|
|
19
|
+
"CODEX_SANDBOX",
|
|
20
|
+
"CODEX_SANDBOX_NETWORK_DISABLED",
|
|
21
|
+
"CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { withoutHarnessMarkers } from "../harness-env.js";
|
|
2
3
|
function runEnvironment(options) {
|
|
3
4
|
if (options.inheritEnv === false) {
|
|
4
5
|
return options.env ?? {};
|
|
@@ -181,7 +182,7 @@ async function spawnAsync(command, args, options, input) {
|
|
|
181
182
|
});
|
|
182
183
|
}
|
|
183
184
|
export function spawnInherited(command, args) {
|
|
184
|
-
const child = spawn(command, args, { stdio: "inherit" });
|
|
185
|
+
const child = spawn(command, args, { stdio: "inherit", env: withoutHarnessMarkers(process.env) });
|
|
185
186
|
child.on("exit", (code, signal) => {
|
|
186
187
|
if (signal) {
|
|
187
188
|
process.kill(process.pid, signal);
|
package/substrate/tmux-stream.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pty from "@homebridge/node-pty-prebuilt-multiarch";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import { withoutHarnessMarkers } from "./harness-env.js";
|
|
3
4
|
import { assertSession, getStatusLineCount, getWindowSize } from "./tmux.js";
|
|
4
5
|
export class TmuxTerminalAttachError extends Error {
|
|
5
6
|
phase;
|
|
@@ -46,7 +47,7 @@ export function attachTmuxTerminalSocket(options) {
|
|
|
46
47
|
cols,
|
|
47
48
|
cwd: options.cwd ?? process.cwd(),
|
|
48
49
|
env: {
|
|
49
|
-
...process.env,
|
|
50
|
+
...withoutHarnessMarkers(process.env),
|
|
50
51
|
TERM: "screen-256color"
|
|
51
52
|
},
|
|
52
53
|
rows
|
package/substrate/tmux.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { isHarnessMarker } from "./harness-env.js";
|
|
4
5
|
import { requireCommand, run, runAsync, runWithInputAsync, spawnInherited } from "./internal/exec.js";
|
|
5
6
|
import { snapshotDescendantPids } from "./internal/process-tree.js";
|
|
6
7
|
const TMUX_INSTALL_HINT = "Install it with: brew install tmux";
|
|
@@ -63,12 +64,14 @@ export async function createSession({ command, cwd, env, name, owner, ownerOptio
|
|
|
63
64
|
// it. When an agent's session started the server (or is the launcher), that includes the session's own harness
|
|
64
65
|
// identity, so a new pane would resolve as that session. A pane's identity is only what its own session sets:
|
|
65
66
|
// every identity variable the server or the launcher holds and the session does not set is unset in the pane.
|
|
66
|
-
|
|
67
|
+
// The harnesses' own marks are the shared list (harness-env); supercode's caller and agent variables are its own.
|
|
68
|
+
const supercodeIdentity = /^(SUPERCODE_CALLER|SUPERCODE_CALLER_NAME|SUPERCODE_LAUNCHED_BY|SUPERCODE_AGENT_[A-Z_]+)$/;
|
|
69
|
+
const identity = (name) => isHarnessMarker(name) || supercodeIdentity.test(name);
|
|
67
70
|
const serverNames = await runAsync("tmux", tmuxArgs(socket, ["show-environment", "-g"]))
|
|
68
71
|
.then((out) => out.split("\n").map((line) => /^([A-Za-z_][A-Za-z0-9_]*)=/.exec(line)?.[1]).filter((name) => Boolean(name)))
|
|
69
72
|
.catch(() => []); // no server yet: the one this creates starts from this launcher's environment
|
|
70
73
|
const inherited = [...new Set([...serverNames, ...Object.keys(process.env)])]
|
|
71
|
-
.filter((name) => identity
|
|
74
|
+
.filter((name) => identity(name) && !(name in sessionEnv));
|
|
72
75
|
const bootstrapDirectories = [];
|
|
73
76
|
const paneBootstrapArgs = (pane) => {
|
|
74
77
|
const commandArgs = command === undefined ? [] : [command.program, ...(command.arguments ?? [])];
|