@stage-labs/metro 0.1.0-beta.83 → 0.1.0-beta.85

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.83",
3
+ "version": "0.1.0-beta.85",
4
4
  "description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,7 +19,10 @@ import {
19
19
  trainEventToMetroEvent,
20
20
  } from '../routes/http.js';
21
21
  import { localAgentKey } from '../stations/materialize.js';
22
- import { fileSource } from '../agents/files.js';
22
+ import { agentsDir, fileSource } from '../agents/files.js';
23
+ import { ConnectorAggregate } from '../connectors/aggregate.js';
24
+ import { setConnectorToolProvider } from '../mcp/connector-tools.js';
25
+ import { invalidateToolSchema } from '../mcp/tool-dispatch.js';
23
26
  import { applyLocalOwner } from './local-owner.js';
24
27
  import { localOwner } from '../agents/file-admin.js';
25
28
  import { ensureStationDeps } from '../stations/runtime-deps.js';
@@ -29,6 +32,7 @@ import {
29
32
  agentLiveness,
30
33
  closeAgentSession,
31
34
  createMetroMcp,
35
+ announceToolSchemaToAll,
32
36
  } from '../mcp/index.js';
33
37
  import { metroCall } from '../mcp/ctx.js';
34
38
  import { gatherAccountsForAgents } from '../mcp/accounts.js';
@@ -114,6 +118,22 @@ function sessionApis(): SessionApis {
114
118
  });
115
119
  }
116
120
 
121
+ let connectors: ConnectorAggregate | null = null;
122
+
123
+ function startConnectors(): void {
124
+ const aggregate = new ConnectorAggregate(agentsDir(), () => {
125
+ invalidateToolSchema();
126
+ announceToolSchemaToAll();
127
+ });
128
+ connectors = aggregate;
129
+ setConnectorToolProvider({
130
+ list: () => aggregate.list(),
131
+ owns: (name) => aggregate.owns(name),
132
+ call: (name, args) => aggregate.call(name, args),
133
+ });
134
+ aggregate.start();
135
+ }
136
+
117
137
  async function main(): Promise<void> {
118
138
  applyLocalOwner();
119
139
  await materializeFrom(fileSource, { allowEmpty: true });
@@ -126,6 +146,7 @@ async function main(): Promise<void> {
126
146
  metroCall,
127
147
  );
128
148
  metroMcp.startInbound();
149
+ startConnectors();
129
150
  startUploadReaper();
130
151
  announceLocalEndpoint();
131
152
  tunnel?.start();
@@ -145,6 +166,7 @@ async function shutdown(): Promise<void> {
145
166
  if (shuttingDown) return;
146
167
  shuttingDown = true;
147
168
  log.info('dispatcher shutting down');
169
+ connectors?.stop();
148
170
  tunnel?.stop();
149
171
  if (webhookServer) {
150
172
  const server = webhookServer;
@@ -0,0 +1,199 @@
1
+ import { watch, type FSWatcher } from 'node:fs';
2
+ import { isRecord } from '@metro-labs/core/is-record';
3
+ import { errMsg, log } from '@metro-labs/core/log';
4
+ import type { ToolResult } from '@metro-labs/core/stations/types';
5
+ import { signInState } from './config.js';
6
+ import type { RelayTarget } from './relay-target.js';
7
+ import { localRelayTarget, readLocalConnectors, type LocalConnectorRow } from './store.js';
8
+ import { UpstreamClient, type UpstreamTool } from './upstream.js';
9
+
10
+ const SEP = '__';
11
+ const SLUG_MAX = 24;
12
+ const NAME_MAX = 64;
13
+ const WATCH_DEBOUNCE_MS = 300;
14
+ const CONNECTORS_FILE = 'connectors.json';
15
+
16
+ export interface ConnectorToolView {
17
+ name: string;
18
+ description: string;
19
+ inputSchema: unknown;
20
+ annotations?: unknown;
21
+ }
22
+
23
+ interface Entry {
24
+ row: LocalConnectorRow;
25
+ stamp: string;
26
+ slug: string;
27
+ client: UpstreamClient;
28
+ tools: UpstreamTool[];
29
+ }
30
+
31
+ type TargetOf = (connectorId: string, force: boolean, dir: string) => Promise<RelayTarget>;
32
+
33
+ export function slugOf(name: string): string {
34
+ const slug = name
35
+ .toLowerCase()
36
+ .replace(/[^a-z0-9]+/g, '_')
37
+ .replace(/^_+|_+$/g, '')
38
+ .slice(0, SLUG_MAX)
39
+ .replace(/_+$/, '');
40
+ return slug === '' ? 'connector' : slug;
41
+ }
42
+
43
+ const stampOf = (row: LocalConnectorRow): string =>
44
+ JSON.stringify([row.name, row.url, row.config.auth.kind, row.config.verified.at, row.config.oauth]);
45
+
46
+ const toolName = (slug: string, tool: string): string => `${slug}${SEP}${tool}`.slice(0, NAME_MAX);
47
+
48
+ const textBlock = (text: string): { type: 'text'; text: string } => ({ type: 'text', text });
49
+
50
+ function blockOf(block: unknown): { type: 'text'; text: string } {
51
+ if (isRecord(block) && block.type === 'text' && typeof block.text === 'string') return textBlock(block.text);
52
+ const kind = isRecord(block) && typeof block.type === 'string' ? block.type : 'unknown';
53
+ return textBlock(`[${kind} block omitted by metro]`);
54
+ }
55
+
56
+ export function toToolResult(raw: unknown): ToolResult {
57
+ if (isRecord(raw) && Array.isArray(raw.content)) {
58
+ const content = raw.content.map(blockOf);
59
+ return raw.isError === true ? { content, isError: true } : { content };
60
+ }
61
+ return { content: [textBlock(JSON.stringify(raw ?? null))] };
62
+ }
63
+
64
+ function dedupeSlugs(entries: Map<string, Entry>): void {
65
+ const bySlug = new Map<string, Entry[]>();
66
+ for (const entry of entries.values()) bySlug.set(entry.slug, [...(bySlug.get(entry.slug) ?? []), entry]);
67
+ for (const group of bySlug.values()) {
68
+ if (group.length < 2) continue;
69
+ for (const entry of group) entry.slug = `${entry.slug}_${entry.row.id.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 4)}`;
70
+ }
71
+ }
72
+
73
+ export class ConnectorAggregate {
74
+ private entries = new Map<string, Entry>();
75
+ private watcher: FSWatcher | null = null;
76
+ private timer: ReturnType<typeof setTimeout> | null = null;
77
+ private loading: Promise<void> | null = null;
78
+
79
+ constructor(
80
+ private readonly dir: string,
81
+ private readonly onChange: () => void,
82
+ private readonly targetOf: TargetOf = localRelayTarget,
83
+ ) {}
84
+
85
+ start(): void {
86
+ try {
87
+ this.watcher = watch(this.dir, (_event, file) => {
88
+ if (file === null || file === CONNECTORS_FILE) this.schedule();
89
+ });
90
+ this.watcher.on('error', (err: unknown) => {
91
+ log.warn({ err: errMsg(err) }, 'connectors: watcher failed; tools refresh on the next daemon start');
92
+ });
93
+ this.watcher.unref();
94
+ } catch (err) {
95
+ log.warn({ err: errMsg(err) }, 'connectors: could not watch the agents dir');
96
+ }
97
+ this.schedule(0);
98
+ }
99
+
100
+ stop(): void {
101
+ this.watcher?.close();
102
+ this.watcher = null;
103
+ if (this.timer !== null) clearTimeout(this.timer);
104
+ this.timer = null;
105
+ }
106
+
107
+ private schedule(ms = WATCH_DEBOUNCE_MS): void {
108
+ if (this.timer !== null) clearTimeout(this.timer);
109
+ this.timer = setTimeout(() => {
110
+ this.timer = null;
111
+ this.reload().catch((err: unknown) => {
112
+ log.warn({ err: errMsg(err) }, 'connectors: reload failed');
113
+ });
114
+ }, ms);
115
+ this.timer.unref();
116
+ }
117
+
118
+ reload(): Promise<void> {
119
+ this.loading ??= this.load().finally(() => {
120
+ this.loading = null;
121
+ });
122
+ return this.loading;
123
+ }
124
+
125
+ private async load(): Promise<void> {
126
+ const rows = readLocalConnectors(this.dir).filter((row) => signInState(row.config) !== 'disconnected');
127
+ const next = new Map<string, Entry>();
128
+ const jobs: Promise<void>[] = [];
129
+ for (const row of rows) {
130
+ const stamp = stampOf(row);
131
+ const kept = this.entries.get(row.id);
132
+ if (kept?.stamp === stamp && kept !== undefined) {
133
+ next.set(row.id, kept);
134
+ continue;
135
+ }
136
+ const entry: Entry = { row, stamp, slug: slugOf(row.name), client: new UpstreamClient((force) => this.targetOf(row.id, force, this.dir)), tools: [] };
137
+ next.set(row.id, entry);
138
+ jobs.push(this.fill(entry));
139
+ }
140
+ await Promise.all(jobs);
141
+ dedupeSlugs(next);
142
+ const before = this.signature();
143
+ this.entries = next;
144
+ if (this.signature() !== before) this.onChange();
145
+ }
146
+
147
+ private async fill(entry: Entry): Promise<void> {
148
+ try {
149
+ entry.tools = await entry.client.listTools();
150
+ log.info({ connector: entry.row.name, tools: entry.tools.length }, 'connectors: tools listed');
151
+ } catch (err) {
152
+ log.warn({ connector: entry.row.name, err: errMsg(err) }, 'connectors: tools not listed; the connector has no tools until it answers');
153
+ }
154
+ }
155
+
156
+ signature(): string {
157
+ return JSON.stringify([...this.entries.values()].map((e) => [e.row.id, e.slug, e.tools.map((t) => t.name)]).sort());
158
+ }
159
+
160
+ list(): ConnectorToolView[] {
161
+ const out: ConnectorToolView[] = [];
162
+ for (const entry of [...this.entries.values()].sort((a, b) => a.slug.localeCompare(b.slug)))
163
+ for (const tool of entry.tools)
164
+ out.push({
165
+ name: toolName(entry.slug, tool.name),
166
+ description: tool.description === '' ? `${entry.row.name}: ${tool.name}` : `${tool.description} (${entry.row.name})`,
167
+ inputSchema: tool.inputSchema,
168
+ ...(tool.annotations === undefined ? {} : { annotations: tool.annotations }),
169
+ });
170
+ return out;
171
+ }
172
+
173
+ private resolve(name: string): { entry: Entry; tool: UpstreamTool } | null {
174
+ const at = name.indexOf(SEP);
175
+ if (at <= 0) return null;
176
+ const slug = name.slice(0, at);
177
+ for (const entry of this.entries.values()) {
178
+ if (entry.slug !== slug) continue;
179
+ const tool = entry.tools.find((t) => toolName(slug, t.name) === name);
180
+ return tool === undefined ? null : { entry, tool };
181
+ }
182
+ return null;
183
+ }
184
+
185
+ owns(name: string): boolean {
186
+ return this.resolve(name) !== null;
187
+ }
188
+
189
+ async call(name: string, args: Record<string, unknown>): Promise<ToolResult> {
190
+ const found = this.resolve(name);
191
+ if (found === null) return { content: [textBlock(`metro: no connector serves ${name}`)], isError: true };
192
+ try {
193
+ return toToolResult(await found.entry.client.callTool(found.tool.name, args));
194
+ } catch (err) {
195
+ log.warn({ connector: found.entry.row.name, tool: found.tool.name, err: errMsg(err) }, 'connectors: tool call failed');
196
+ return { content: [textBlock(`metro: connector ${found.entry.row.name} ${errMsg(err)}`)], isError: true };
197
+ }
198
+ }
199
+ }
@@ -0,0 +1,172 @@
1
+ import { isRecord } from '@metro-labs/core/is-record';
2
+ import { errMsg } from '@metro-labs/core/log';
3
+ import type { RelayTarget } from './relay-target.js';
4
+
5
+ const ACCEPT = 'application/json, text/event-stream';
6
+ const PROTOCOL = '2025-11-25';
7
+ const MAX_PAGES = 10;
8
+ const LIST_MS = 15_000;
9
+ const CALL_MS = 120_000;
10
+
11
+ export interface UpstreamTool {
12
+ name: string;
13
+ description: string;
14
+ inputSchema: unknown;
15
+ annotations: unknown;
16
+ }
17
+
18
+ export type TargetOf = (force: boolean) => Promise<RelayTarget>;
19
+
20
+ export class UpstreamRefused extends Error {}
21
+ export class UpstreamFailed extends Error {}
22
+
23
+ interface Live {
24
+ session: string | null;
25
+ protocol: string;
26
+ }
27
+
28
+ interface Answer {
29
+ status: number;
30
+ text: string;
31
+ contentType: string;
32
+ session: string | null;
33
+ }
34
+
35
+ function dataLines(text: string): string[] {
36
+ return text
37
+ .replace(/\r\n/g, '\n')
38
+ .split('\n')
39
+ .filter((line) => line.startsWith('data:'))
40
+ .map((line) => line.slice(5).trim());
41
+ }
42
+
43
+ function messageFor(answer: Answer, id: number): Record<string, unknown> | null {
44
+ const candidates = answer.contentType.includes('text/event-stream') ? dataLines(answer.text) : [answer.text];
45
+ for (const candidate of candidates) {
46
+ try {
47
+ const parsed: unknown = JSON.parse(candidate);
48
+ if (isRecord(parsed) && parsed.id === id) return parsed;
49
+ } catch {
50
+ continue;
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+
56
+ function resultOf(answer: Answer, id: number): unknown {
57
+ const message = messageFor(answer, id);
58
+ if (message === null) throw new UpstreamFailed('answered without a result for the request');
59
+ if (isRecord(message.error)) {
60
+ const text = typeof message.error.message === 'string' ? message.error.message : JSON.stringify(message.error);
61
+ throw new UpstreamFailed(text);
62
+ }
63
+ return message.result;
64
+ }
65
+
66
+ const initializeBody = (id: number): unknown => ({
67
+ jsonrpc: '2.0',
68
+ id,
69
+ method: 'initialize',
70
+ params: { protocolVersion: PROTOCOL, capabilities: {}, clientInfo: { name: 'metro', version: '0.1.0' } },
71
+ });
72
+
73
+ function toolOf(raw: unknown): UpstreamTool | null {
74
+ if (!isRecord(raw) || typeof raw.name !== 'string' || raw.name === '') return null;
75
+ return {
76
+ name: raw.name,
77
+ description: typeof raw.description === 'string' ? raw.description : '',
78
+ inputSchema: isRecord(raw.inputSchema) ? raw.inputSchema : { type: 'object' },
79
+ annotations: isRecord(raw.annotations) ? raw.annotations : undefined,
80
+ };
81
+ }
82
+
83
+ export class UpstreamClient {
84
+ private live: Live | null = null;
85
+ private seq = 0;
86
+
87
+ constructor(private readonly target: TargetOf) {}
88
+
89
+ forget(): void {
90
+ this.live = null;
91
+ }
92
+
93
+ async listTools(): Promise<UpstreamTool[]> {
94
+ const tools: UpstreamTool[] = [];
95
+ let cursor = '';
96
+ for (let page = 0; page < MAX_PAGES; page += 1) {
97
+ const result = await this.rpc('tools/list', cursor === '' ? {} : { cursor }, LIST_MS);
98
+ if (!isRecord(result)) break;
99
+ if (Array.isArray(result.tools)) for (const raw of result.tools) {
100
+ const tool = toolOf(raw);
101
+ if (tool !== null) tools.push(tool);
102
+ }
103
+ const next = result.nextCursor;
104
+ if (typeof next !== 'string' || next === '' || next === cursor) break;
105
+ cursor = next;
106
+ }
107
+ return tools;
108
+ }
109
+
110
+ callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
111
+ return this.rpc('tools/call', { name, arguments: args }, CALL_MS);
112
+ }
113
+
114
+ private async post(body: unknown, headers: Record<string, string>, force: boolean, ms: number): Promise<Answer> {
115
+ const target = await this.target(force);
116
+ if (target.kind === 'missing') throw new UpstreamRefused('this connector no longer exists on the daemon');
117
+ if (target.kind === 'signin') throw new UpstreamRefused('this connector needs signing in again on its page');
118
+ let res: Response;
119
+ try {
120
+ res = await fetch(target.url, {
121
+ method: 'POST',
122
+ redirect: 'manual',
123
+ signal: AbortSignal.timeout(ms),
124
+ headers: { 'content-type': 'application/json', accept: ACCEPT, ...target.headers, ...headers },
125
+ body: JSON.stringify(body),
126
+ });
127
+ } catch (err) {
128
+ throw new UpstreamFailed(`could not be reached (${errMsg(err)})`);
129
+ }
130
+ const text = await res.text().catch(() => '');
131
+ return { status: res.status, text, contentType: res.headers.get('content-type') ?? '', session: res.headers.get('mcp-session-id') };
132
+ }
133
+
134
+ private async withCredentialLadder(body: unknown, headers: Record<string, string>, ms: number): Promise<Answer> {
135
+ const first = await this.post(body, headers, false, ms);
136
+ if (first.status !== 401 && first.status !== 403) return first;
137
+ const again = await this.post(body, headers, true, ms);
138
+ if (again.status === 401 || again.status === 403) throw new UpstreamRefused('rejected the credential; sign in again on its page');
139
+ return again;
140
+ }
141
+
142
+ private async ensureSession(): Promise<Live> {
143
+ if (this.live !== null) return this.live;
144
+ const id = ++this.seq;
145
+ const answer = await this.withCredentialLadder(initializeBody(id), {}, LIST_MS);
146
+ if (answer.status >= 300) throw new UpstreamFailed(`answered ${String(answer.status)} to initialize`);
147
+ const result = resultOf(answer, id);
148
+ const protocol = isRecord(result) && typeof result.protocolVersion === 'string' ? result.protocolVersion : PROTOCOL;
149
+ this.live = { session: answer.session, protocol };
150
+ await this.post({ jsonrpc: '2.0', method: 'notifications/initialized' }, this.sessionHeaders(this.live), false, LIST_MS).catch(() => undefined);
151
+ return this.live;
152
+ }
153
+
154
+ private sessionHeaders(live: Live): Record<string, string> {
155
+ const headers: Record<string, string> = { 'mcp-protocol-version': live.protocol };
156
+ if (live.session !== null && live.session !== '') headers['mcp-session-id'] = live.session;
157
+ return headers;
158
+ }
159
+
160
+ private async rpc(method: string, params: unknown, ms: number): Promise<unknown> {
161
+ const live = await this.ensureSession();
162
+ const id = ++this.seq;
163
+ const body = { jsonrpc: '2.0', id, method, params };
164
+ let answer = await this.withCredentialLadder(body, this.sessionHeaders(live), ms);
165
+ if (answer.status === 404 && live.session !== null) {
166
+ this.live = null;
167
+ answer = await this.withCredentialLadder(body, this.sessionHeaders(await this.ensureSession()), ms);
168
+ }
169
+ if (answer.status >= 300) throw new UpstreamFailed(`answered ${String(answer.status)}${answer.text === '' ? '' : `: ${answer.text.slice(0, 200)}`}`);
170
+ return resultOf(answer, id);
171
+ }
172
+ }
@@ -0,0 +1,31 @@
1
+ import type { ToolResult } from '@metro-labs/core/stations/types';
2
+ import { errResult } from './ctx.js';
3
+
4
+ export interface ConnectorToolEntry {
5
+ name: string;
6
+ description: string;
7
+ inputSchema: unknown;
8
+ annotations?: unknown;
9
+ }
10
+
11
+ export interface ConnectorToolProvider {
12
+ list: () => ConnectorToolEntry[];
13
+ owns: (name: string) => boolean;
14
+ call: (name: string, args: Record<string, unknown>) => Promise<ToolResult>;
15
+ }
16
+
17
+ const NONE: ConnectorToolProvider = {
18
+ list: () => [],
19
+ owns: () => false,
20
+ call: (name) => Promise.resolve(errResult(`metro: no connector serves ${name}`)),
21
+ };
22
+
23
+ let provider: ConnectorToolProvider = NONE;
24
+
25
+ export function setConnectorToolProvider(next: ConnectorToolProvider | null): void {
26
+ provider = next ?? NONE;
27
+ }
28
+
29
+ export const connectorToolList = (): ConnectorToolEntry[] => provider.list();
30
+ export const isConnectorTool = (name: string): boolean => provider.owns(name);
31
+ export const callConnectorTool = (name: string, args: Record<string, unknown>): Promise<ToolResult> => provider.call(name, args);
@@ -138,6 +138,10 @@ export function agentLiveness(): Map<string, AgentLiveness> {
138
138
  return activeRegistry?.liveness() ?? new Map<string, AgentLiveness>();
139
139
  }
140
140
 
141
+ export function announceToolSchemaToAll(): void {
142
+ activeRegistry?.announceToolSchema();
143
+ }
144
+
141
145
  export async function closeAgentSession(agentId: string): Promise<boolean> {
142
146
  const scopeKey = sessionScopeKey({ kind: 'agent', agentId });
143
147
  return (await activeRegistry?.closeScope(scopeKey)) ?? false;
@@ -61,6 +61,10 @@ export class SessionRegistry {
61
61
  return out;
62
62
  }
63
63
 
64
+ announceToolSchema(): void {
65
+ for (const session of this.byId.values()) session.announceToolSchema();
66
+ }
67
+
64
68
  forScope(scopeKey: string): McpSession | undefined {
65
69
  return this.byScope.get(scopeKey);
66
70
  }
@@ -165,7 +165,7 @@ export class McpSession {
165
165
  return this.issuedSchema !== toolSchemaSignature();
166
166
  }
167
167
 
168
- private announceToolSchema(): void {
168
+ announceToolSchema(): void {
169
169
  if (!this.streamAttached) return;
170
170
  this.deliverSchemaNotice(
171
171
  () => this.server.sendToolListChanged(),
@@ -31,6 +31,7 @@ import {
31
31
  type RequestIdentity,
32
32
  } from './request-identity.js';
33
33
  import { str } from '@metro-labs/core/str';
34
+ import { callConnectorTool, connectorToolList, isConnectorTool } from './connector-tools.js';
34
35
 
35
36
  const STATION_TOOLS = new Map<
36
37
  string,
@@ -62,11 +63,16 @@ const toolList = (): { tools: unknown[] } => ({
62
63
  })),
63
64
  ),
64
65
  LIST_ACCOUNTS_TOOL,
66
+ ...connectorToolList(),
65
67
  ],
66
68
  });
67
69
 
68
70
  let schemaSignature: string | undefined;
69
71
 
72
+ export function invalidateToolSchema(): void {
73
+ schemaSignature = undefined;
74
+ }
75
+
70
76
  export const toolSchemaSignature = (): string => {
71
77
  schemaSignature ??= createHash('sha256')
72
78
  .update(JSON.stringify(toolList()))
@@ -149,6 +155,7 @@ async function runTool(
149
155
  const name = req.params.name;
150
156
  const a = req.params.arguments ?? {};
151
157
 
158
+ if (isConnectorTool(name)) return callConnectorTool(name, a);
152
159
  const identity = currentIdentity();
153
160
  if (name !== 'list_accounts' && scopeDenied(identity, name, a))
154
161
  return errResult('metro: this account is outside your authorized scope');
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import { homedir } from 'node:os';
2
3
  import type { IncomingMessage, ServerResponse } from 'node:http';
3
4
  import { ApiError } from '@metro-labs/http/api-error';
4
5
  import { apiFailure, apiSession, bodyField, cors, readJsonBody, sendJson } from '@metro-labs/http/api-http';
@@ -9,7 +10,26 @@ const PREFIX = '/api/terminal';
9
10
  const TICKETS = `${PREFIX}/tickets`;
10
11
  const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
11
12
 
12
- export const tmuxCommand = (session: string): string[] => ['tmux', 'new-session', '-A', '-D', '-s', session, ';', 'set-option', '-g', 'mouse', 'on'];
13
+ export const tmuxCommand = (session: string, home = homedir()): string[] => [
14
+ 'tmux',
15
+ 'new-session',
16
+ '-A',
17
+ '-D',
18
+ '-s',
19
+ session,
20
+ '-c',
21
+ home,
22
+ ';',
23
+ 'set-option',
24
+ '-g',
25
+ 'mouse',
26
+ 'on',
27
+ ';',
28
+ 'set-option',
29
+ '-s',
30
+ 'set-clipboard',
31
+ 'on',
32
+ ];
13
33
 
14
34
  export interface TerminalApiDeps {
15
35
  authorize: (subject: string) => void;
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.83"
2
+ "version": "0.1.0-beta.85"
3
3
  }