@osdlc/agent-seat 0.0.0-stage → 0.7.1
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/CHANGELOG.md +133 -0
- package/CONTRIBUTING.md +44 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +181 -2
- package/SECURITY.md +41 -0
- package/dist/contract.d.ts +297 -0
- package/dist/contract.js +102 -0
- package/dist/harness.d.ts +305 -0
- package/dist/harness.js +610 -0
- package/dist/hosts/cmux.d.ts +67 -0
- package/dist/hosts/cmux.js +252 -0
- package/dist/hosts/herdr.d.ts +78 -0
- package/dist/hosts/herdr.js +324 -0
- package/dist/hosts/jetbrains.d.ts +98 -0
- package/dist/hosts/jetbrains.js +353 -0
- package/dist/hosts/orca.d.ts +67 -0
- package/dist/hosts/orca.js +292 -0
- package/dist/hosts/otty.d.ts +70 -0
- package/dist/hosts/otty.js +233 -0
- package/dist/hosts/registry.d.ts +50 -0
- package/dist/hosts/registry.js +100 -0
- package/dist/hosts/session-panes.d.ts +29 -0
- package/dist/hosts/session-panes.js +49 -0
- package/dist/hosts/terminal.d.ts +23 -0
- package/dist/hosts/terminal.js +47 -0
- package/dist/hosts/tmux.d.ts +145 -0
- package/dist/hosts/tmux.js +409 -0
- package/dist/hosts/vscode-client.d.ts +81 -0
- package/dist/hosts/vscode-client.js +124 -0
- package/dist/hosts/vscode.d.ts +35 -0
- package/dist/hosts/vscode.js +127 -0
- package/dist/hosts/wezterm.d.ts +78 -0
- package/dist/hosts/wezterm.js +405 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +61 -0
- package/dist/open-workspace.d.ts +9 -0
- package/dist/open-workspace.js +12 -0
- package/dist/util.d.ts +50 -0
- package/dist/util.js +108 -0
- package/package.json +76 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@osdlc/agent-seat`: put an interactive coding-agent session in front of a
|
|
3
|
+
* person, in the terminal app, multiplexer or editor they already use.
|
|
4
|
+
*
|
|
5
|
+
* A {@link Host} is one place a session can open: a terminal tab, a tmux
|
|
6
|
+
* session, a WezTerm pane, a VS Code terminal. A host fulfils the contract in
|
|
7
|
+
* parts (open a session, run a command, send text, find, focus, capture,
|
|
8
|
+
* close); a caller asks for the part it needs with {@link requirePart} and gets
|
|
9
|
+
* a {@link HostPartMissing} error when the host lacks it, never a silent
|
|
10
|
+
* fallback to another host.
|
|
11
|
+
*
|
|
12
|
+
* A {@link Harness} is the agent CLI the session runs (claude, codex, ...).
|
|
13
|
+
* Every host runs any harness through {@link harnessArgv}, so where a session
|
|
14
|
+
* opens and which agent runs in it are independent choices.
|
|
15
|
+
*
|
|
16
|
+
* {@link hostRegistry} builds every built-in host. Hosts that remember panes or
|
|
17
|
+
* read an IDE companion's registry keep their files under the caller's
|
|
18
|
+
* `runtimeDir`.
|
|
19
|
+
*/
|
|
20
|
+
import { makeCmuxHost } from "./hosts/cmux.js";
|
|
21
|
+
import { makeHerdrHost } from "./hosts/herdr.js";
|
|
22
|
+
import { makeJetbrainsHost } from "./hosts/jetbrains.js";
|
|
23
|
+
import { makeOrcaHost } from "./hosts/orca.js";
|
|
24
|
+
import { makeOttyHost } from "./hosts/otty.js";
|
|
25
|
+
import { terminalHost } from "./hosts/terminal.js";
|
|
26
|
+
import { makeTmuxHost } from "./hosts/tmux.js";
|
|
27
|
+
import { makeVscodeHost } from "./hosts/vscode.js";
|
|
28
|
+
import { makeWeztermHost } from "./hosts/wezterm.js";
|
|
29
|
+
export { HOST_NAMES, HostNameSchema, HostPartMissing, WAIT_TIMEOUT_EXIT, hostCanWait, onPath, requirePart, supportsPlatform, } from "./contract.js";
|
|
30
|
+
export { CLAUDE_HOOK_EVENTS, ClaudeSessionFlagsSchema, HarnessSchema, claudeHookSettings, claudeResumeArgv, claudeSessionArgv, codexSessionsDir, findCodexSessionId, harnessArgv, harnessAvailable, harnessNativeSend, harnessOrNull, hasInteractiveArgv, parseRolloutMeta, walkRolloutFiles, withEnv, wrapperSession, } from "./harness.js";
|
|
31
|
+
export { ensureWorkspace } from "./open-workspace.js";
|
|
32
|
+
export { makeCmuxHost } from "./hosts/cmux.js";
|
|
33
|
+
export { makeHerdrHost } from "./hosts/herdr.js";
|
|
34
|
+
export { JETBRAINS_PROTOCOL, makeJetbrainsHost } from "./hosts/jetbrains.js";
|
|
35
|
+
export { ORCA_BUNDLED_CLI, makeOrcaHost } from "./hosts/orca.js";
|
|
36
|
+
export { makeOttyHost } from "./hosts/otty.js";
|
|
37
|
+
export { hostRegistryDir } from "./hosts/registry.js";
|
|
38
|
+
export { makeTerminalHost, terminalHost } from "./hosts/terminal.js";
|
|
39
|
+
export { insideTmux, makeTmuxHost, tmuxAttachArgv, tmuxIdSessionName, tmuxSessionName, tmuxSwitchClientArgv, } from "./hosts/tmux.js";
|
|
40
|
+
export { makeVscodeHost } from "./hosts/vscode.js";
|
|
41
|
+
export { liveWindows, subprocessTransport, } from "./hosts/vscode-client.js";
|
|
42
|
+
export { makeWeztermHost } from "./hosts/wezterm.js";
|
|
43
|
+
/**
|
|
44
|
+
* Every built-in host, by name. Each one is a wrapper: it opens a terminal or
|
|
45
|
+
* workspace and runs whichever harness the request names, so none hardcodes an
|
|
46
|
+
* agent CLI. A consumer that wants a fake builds its own `HostRegistry`.
|
|
47
|
+
*/
|
|
48
|
+
export function hostRegistry(opts) {
|
|
49
|
+
const { runtimeDir } = opts;
|
|
50
|
+
return {
|
|
51
|
+
terminal: terminalHost,
|
|
52
|
+
orca: makeOrcaHost({ repoRoot: opts.repoRoot, runtimeDir }),
|
|
53
|
+
cmux: makeCmuxHost({}),
|
|
54
|
+
tmux: makeTmuxHost({}),
|
|
55
|
+
herdr: makeHerdrHost({ runtimeDir }),
|
|
56
|
+
otty: makeOttyHost(),
|
|
57
|
+
wezterm: makeWeztermHost({ runtimeDir }),
|
|
58
|
+
vscode: makeVscodeHost({ runtimeDir }),
|
|
59
|
+
jetbrains: makeJetbrainsHost({ runtimeDir, settings: opts.jetbrains }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one policy for opening a host's workspace before a launch: when
|
|
3
|
+
* `host.open_workspace` is on and the host fulfils the optional
|
|
4
|
+
* `openWorkspace` part, call it before `session`/`terminal`. `send`, `find`
|
|
5
|
+
* and `focus` never come through here.
|
|
6
|
+
*/
|
|
7
|
+
import type { Host } from './contract.ts';
|
|
8
|
+
/** Opens a workspace for `cwd` on `host` when `enabled` and the host can; a host without the part is left alone. */
|
|
9
|
+
export declare function ensureWorkspace(host: Host, cwd: string, enabled: boolean, notice: (line: string) => void): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one policy for opening a host's workspace before a launch: when
|
|
3
|
+
* `host.open_workspace` is on and the host fulfils the optional
|
|
4
|
+
* `openWorkspace` part, call it before `session`/`terminal`. `send`, `find`
|
|
5
|
+
* and `focus` never come through here.
|
|
6
|
+
*/
|
|
7
|
+
/** Opens a workspace for `cwd` on `host` when `enabled` and the host can; a host without the part is left alone. */
|
|
8
|
+
export function ensureWorkspace(host, cwd, enabled, notice) {
|
|
9
|
+
if (!enabled || host.openWorkspace === undefined)
|
|
10
|
+
return;
|
|
11
|
+
host.openWorkspace({ cwd, notice });
|
|
12
|
+
}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small private helpers the hosts share. Nothing here is exported from the
|
|
3
|
+
* package entry.
|
|
4
|
+
*/
|
|
5
|
+
/** A plain object (not null, not an array). */
|
|
6
|
+
export declare function isRecord(v: unknown): v is Record<string, unknown>;
|
|
7
|
+
/** `v` when it is a string, else null. */
|
|
8
|
+
export declare function strOrNull(v: unknown): string | null;
|
|
9
|
+
/** A non-empty string, else null. */
|
|
10
|
+
export declare function nonEmptyString(v: unknown): string | null;
|
|
11
|
+
/** `JSON.parse(text)`, or null when it is not valid JSON. */
|
|
12
|
+
export declare function parseJsonOrNull(text: string): unknown;
|
|
13
|
+
/** The parsed JSON in `path`, or null when it is missing, unreadable or not JSON. */
|
|
14
|
+
export declare function readJsonOrNull(path: string): unknown;
|
|
15
|
+
/** Write `data` as JSON to `path` atomically (temp file, fsync, rename). */
|
|
16
|
+
export declare function writeJsonAtomic(path: string, data: unknown): void;
|
|
17
|
+
/** Create `dir` and its parents; an existing directory is fine. */
|
|
18
|
+
export declare function ensureDir(dir: string): void;
|
|
19
|
+
/** Block the thread for `ms` milliseconds. For sync code that backs off between polls. */
|
|
20
|
+
export declare function sleepSync(ms: number): void;
|
|
21
|
+
/**
|
|
22
|
+
* True when `pid` names a running process. Signal 0 delivers nothing; it only
|
|
23
|
+
* asks the OS whether the process exists. `EPERM` means it exists but belongs
|
|
24
|
+
* to another user, so it is alive. Pids are recycled, so a reused pid reads as
|
|
25
|
+
* alive.
|
|
26
|
+
*/
|
|
27
|
+
export declare function pidAlive(pid: number): boolean;
|
|
28
|
+
/** `bin`'s path on `PATH` (PATHEXT-aware on Windows), or null. */
|
|
29
|
+
export declare function which(bin: string): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* True when this module runs inside a `bun build --compile` standalone binary,
|
|
32
|
+
* read from a module URL (pass `import.meta.url`).
|
|
33
|
+
*/
|
|
34
|
+
export declare function isCompiledBinary(metaUrl: string): boolean;
|
|
35
|
+
/** The first `hexChars` lowercase hex digits of the sha256 of `text`. */
|
|
36
|
+
export declare function shortHash(text: string, hexChars: number): string;
|
|
37
|
+
/** `text` with anything outside `[A-Za-z0-9_-]` rewritten to `_`. */
|
|
38
|
+
export declare function safeName(text: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* A collision-free stand-in for a session id where the id itself cannot go (a tmux session
|
|
41
|
+
* name, a title tag, a file name): the id's first 12 characters through {@link safeName}, for a
|
|
42
|
+
* person to read, then `-` and the first `hashChars` hex digits of the sha256 of the whole id.
|
|
43
|
+
* The hash keeps apart ids that share a prefix, differ only in a rewritten character, or differ
|
|
44
|
+
* only in case.
|
|
45
|
+
*/
|
|
46
|
+
export declare function sessionIdSlug(sessionId: string, hashChars?: number): string;
|
|
47
|
+
/** The real path of `p`: symlinks in the part that exists resolved, the rest re-attached. */
|
|
48
|
+
export declare function realResolve(p: string): string;
|
|
49
|
+
/** Whether `child` is `parent` or inside it, comparing real paths. */
|
|
50
|
+
export declare function isWithin(child: string, parent: string): boolean;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small private helpers the hosts share. Nothing here is exported from the
|
|
3
|
+
* package entry.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { mkdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
8
|
+
import { which as whichOn } from '@osdlc/detect-harness';
|
|
9
|
+
import { atomicWrite } from '@osdlc/etag-write';
|
|
10
|
+
import { nodeGuard } from '@osdlc/path-guard/node';
|
|
11
|
+
/** A plain object (not null, not an array). */
|
|
12
|
+
export function isRecord(v) {
|
|
13
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
14
|
+
}
|
|
15
|
+
/** `v` when it is a string, else null. */
|
|
16
|
+
export function strOrNull(v) {
|
|
17
|
+
return typeof v === 'string' ? v : null;
|
|
18
|
+
}
|
|
19
|
+
/** A non-empty string, else null. */
|
|
20
|
+
export function nonEmptyString(v) {
|
|
21
|
+
return typeof v === 'string' && v !== '' ? v : null;
|
|
22
|
+
}
|
|
23
|
+
/** `JSON.parse(text)`, or null when it is not valid JSON. */
|
|
24
|
+
export function parseJsonOrNull(text) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(text);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** The parsed JSON in `path`, or null when it is missing, unreadable or not JSON. */
|
|
33
|
+
export function readJsonOrNull(path) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Write `data` as JSON to `path` atomically (temp file, fsync, rename). */
|
|
42
|
+
export function writeJsonAtomic(path, data) {
|
|
43
|
+
atomicWrite(path, JSON.stringify(data, null, 2));
|
|
44
|
+
}
|
|
45
|
+
/** Create `dir` and its parents; an existing directory is fine. */
|
|
46
|
+
export function ensureDir(dir) {
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
/** Block the thread for `ms` milliseconds. For sync code that backs off between polls. */
|
|
50
|
+
export function sleepSync(ms) {
|
|
51
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* True when `pid` names a running process. Signal 0 delivers nothing; it only
|
|
55
|
+
* asks the OS whether the process exists. `EPERM` means it exists but belongs
|
|
56
|
+
* to another user, so it is alive. Pids are recycled, so a reused pid reads as
|
|
57
|
+
* alive.
|
|
58
|
+
*/
|
|
59
|
+
export function pidAlive(pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return e.code === 'EPERM';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** `bin`'s path on `PATH` (PATHEXT-aware on Windows), or null. */
|
|
69
|
+
export function which(bin) {
|
|
70
|
+
return whichOn(bin, process.env);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* True when this module runs inside a `bun build --compile` standalone binary,
|
|
74
|
+
* read from a module URL (pass `import.meta.url`).
|
|
75
|
+
*/
|
|
76
|
+
export function isCompiledBinary(metaUrl) {
|
|
77
|
+
return metaUrl.startsWith('file:///$bunfs/') || /^file:\/\/\/[A-Za-z]:\/~BUN\//.test(metaUrl);
|
|
78
|
+
}
|
|
79
|
+
/** The first `hexChars` lowercase hex digits of the sha256 of `text`. */
|
|
80
|
+
export function shortHash(text, hexChars) {
|
|
81
|
+
return createHash('sha256').update(text).digest('hex').slice(0, hexChars);
|
|
82
|
+
}
|
|
83
|
+
/** `text` with anything outside `[A-Za-z0-9_-]` rewritten to `_`. */
|
|
84
|
+
export function safeName(text) {
|
|
85
|
+
return text.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A collision-free stand-in for a session id where the id itself cannot go (a tmux session
|
|
89
|
+
* name, a title tag, a file name): the id's first 12 characters through {@link safeName}, for a
|
|
90
|
+
* person to read, then `-` and the first `hashChars` hex digits of the sha256 of the whole id.
|
|
91
|
+
* The hash keeps apart ids that share a prefix, differ only in a rewritten character, or differ
|
|
92
|
+
* only in case.
|
|
93
|
+
*/
|
|
94
|
+
export function sessionIdSlug(sessionId, hashChars = 8) {
|
|
95
|
+
return `${safeName(sessionId.slice(0, 12))}-${shortHash(sessionId, hashChars)}`;
|
|
96
|
+
}
|
|
97
|
+
/** `p` made absolute against the cwd. */
|
|
98
|
+
function absolute(p) {
|
|
99
|
+
return isAbsolute(p) && process.platform !== 'win32' ? p : resolve(p);
|
|
100
|
+
}
|
|
101
|
+
/** The real path of `p`: symlinks in the part that exists resolved, the rest re-attached. */
|
|
102
|
+
export function realResolve(p) {
|
|
103
|
+
return nodeGuard.realPath(absolute(p));
|
|
104
|
+
}
|
|
105
|
+
/** Whether `child` is `parent` or inside it, comparing real paths. */
|
|
106
|
+
export function isWithin(child, parent) {
|
|
107
|
+
return nodeGuard.contains(absolute(parent), absolute(child));
|
|
108
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,79 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osdlc/agent-seat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "Open an interactive coding-agent session (claude, codex, cursor, pi and others) in the terminal app, multiplexer or editor a person already uses: a terminal tab, tmux, WezTerm, cmux, herdr, Orca, otty, VS Code or a JetBrains IDE. One host contract in optional parts, any harness in any host.",
|
|
5
|
+
"license": "MIT OR Apache-2.0",
|
|
6
|
+
"author": "sksizer",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/osdlc/osdlc.git",
|
|
10
|
+
"directory": "packages/ts/agent-seat"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/osdlc/osdlc/tree/main/packages/ts/agent-seat#readme",
|
|
13
|
+
"bugs": "https://github.com/osdlc/osdlc/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"agent",
|
|
16
|
+
"coding-agent",
|
|
17
|
+
"claude",
|
|
18
|
+
"codex",
|
|
19
|
+
"terminal",
|
|
20
|
+
"tmux",
|
|
21
|
+
"wezterm",
|
|
22
|
+
"vscode",
|
|
23
|
+
"jetbrains",
|
|
24
|
+
"session"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist/",
|
|
39
|
+
"README.md",
|
|
40
|
+
"CHANGELOG.md",
|
|
41
|
+
"LICENSE-MIT",
|
|
42
|
+
"LICENSE-APACHE",
|
|
43
|
+
"SECURITY.md",
|
|
44
|
+
"CONTRIBUTING.md"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"zod": "^4.6.6",
|
|
51
|
+
"@osdlc/agent-call": "^0.9.0",
|
|
52
|
+
"@osdlc/command-seam": "^0.14.0",
|
|
53
|
+
"@osdlc/detect-harness": "^0.7.0",
|
|
54
|
+
"@osdlc/etag-write": "^0.3.1",
|
|
55
|
+
"@osdlc/path-guard": "^0.3.1",
|
|
56
|
+
"@osdlc/typed-git": "^0.13.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^26.1.0",
|
|
60
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
61
|
+
"oxlint": "^1.65.0",
|
|
62
|
+
"prettier": "3.9.6",
|
|
63
|
+
"typescript": "~6.0.3",
|
|
64
|
+
"vitest": "^5.0.5",
|
|
65
|
+
"@osdlc/prettier-config": "^0.3.1",
|
|
66
|
+
"@osdlc/tsconfig": "^0.4.1"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "tsc -p tsconfig.build.json",
|
|
70
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"smoke": "node tests/smoke.mjs",
|
|
73
|
+
"coverage": "vitest run --coverage",
|
|
74
|
+
"format": "prettier --write .",
|
|
75
|
+
"format:check": "prettier --check .",
|
|
76
|
+
"lint": "oxlint .",
|
|
77
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.build.json --noEmit"
|
|
78
|
+
}
|
|
6
79
|
}
|