sessioncast-cli 2.0.9 → 2.1.0

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.
@@ -20,6 +20,13 @@ export declare class ApiWebSocketClient {
20
20
  private handleLlmChat;
21
21
  private handleSendKeys;
22
22
  private handleListSessions;
23
+ /**
24
+ * Handle capability_request from relay — evaluate each requested capability
25
+ * against the agent's config and respond with capability_grant.
26
+ *
27
+ * Config values: true = auto-grant, false = auto-deny, 'ask' = deny + log notice
28
+ */
29
+ private handleCapabilityRequest;
23
30
  private sendApiResponse;
24
31
  private send;
25
32
  private scheduleReconnect;
@@ -138,6 +138,9 @@ class ApiWebSocketClient {
138
138
  case 'list_sessions':
139
139
  await this.handleListSessions(message);
140
140
  break;
141
+ case 'capability_request':
142
+ this.handleCapabilityRequest(message);
143
+ break;
141
144
  }
142
145
  }
143
146
  async handleExec(message) {
@@ -212,7 +215,18 @@ class ApiWebSocketClient {
212
215
  try {
213
216
  console.log('[API] list_sessions');
214
217
  const sessions = tmux.listSessions();
215
- this.sendApiResponse(meta.requestId, { sessions });
218
+ const enriched = sessions.map(s => {
219
+ const cwd = tmux.getPaneCwd(s.name);
220
+ const git = cwd ? tmux.getGitInfo(cwd) : null;
221
+ return {
222
+ ...s,
223
+ cwd: cwd || undefined,
224
+ gitBranch: git?.branch || undefined,
225
+ gitRemote: git?.remote || undefined,
226
+ gitRepo: git?.repo || undefined
227
+ };
228
+ });
229
+ this.sendApiResponse(meta.requestId, { sessions: enriched });
216
230
  }
217
231
  catch (error) {
218
232
  this.sendApiResponse(meta.requestId, {
@@ -221,6 +235,47 @@ class ApiWebSocketClient {
221
235
  });
222
236
  }
223
237
  }
238
+ /**
239
+ * Handle capability_request from relay — evaluate each requested capability
240
+ * against the agent's config and respond with capability_grant.
241
+ *
242
+ * Config values: true = auto-grant, false = auto-deny, 'ask' = deny + log notice
243
+ */
244
+ handleCapabilityRequest(message) {
245
+ const meta = message.meta;
246
+ if (!meta)
247
+ return;
248
+ const from = meta.from;
249
+ const requestedStr = meta.capabilities || '';
250
+ const requested = requestedStr.split(',').map(s => s.trim()).filter(Boolean);
251
+ const capConfig = this.apiConfig.capabilities || {};
252
+ const granted = [];
253
+ const denied = [];
254
+ for (const cap of requested) {
255
+ const setting = capConfig[cap];
256
+ if (setting === true) {
257
+ granted.push(cap);
258
+ }
259
+ else if (setting === 'ask') {
260
+ // Future: interactive prompt. For now, deny and log notice.
261
+ console.log(`[API] Capability '${cap}' requires user consent (configured as 'ask'). Denying by default.`);
262
+ denied.push(cap);
263
+ }
264
+ else if (setting === false || setting === undefined) {
265
+ // Explicitly denied or not configured → deny
266
+ denied.push(cap);
267
+ }
268
+ }
269
+ console.log(`[API] Capability request from SDK: requested=[${requested}], granted=[${granted}], denied=[${denied}]`);
270
+ this.send({
271
+ type: 'capability_grant',
272
+ meta: {
273
+ from: from || '',
274
+ granted: granted.join(','),
275
+ denied: denied.join(',')
276
+ }
277
+ });
278
+ }
224
279
  sendApiResponse(requestId, response) {
225
280
  this.send({
226
281
  type: 'api_response',
@@ -0,0 +1,3 @@
1
+ export declare function setDebug(enabled: boolean): void;
2
+ export declare function isDebug(): boolean;
3
+ export declare function debugLog(tag: string, ...args: any[]): void;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setDebug = setDebug;
4
+ exports.isDebug = isDebug;
5
+ exports.debugLog = debugLog;
6
+ let debugEnabled = false;
7
+ function setDebug(enabled) {
8
+ debugEnabled = enabled;
9
+ }
10
+ function isDebug() {
11
+ return debugEnabled;
12
+ }
13
+ function debugLog(tag, ...args) {
14
+ if (debugEnabled) {
15
+ console.log(`[DEBUG][${tag}]`, ...args);
16
+ }
17
+ }
@@ -17,6 +17,8 @@ export declare class TmuxSessionHandler {
17
17
  private captureTimer;
18
18
  private lastPaneIds;
19
19
  private lastPaneScreens;
20
+ private currentMetaJson;
21
+ private metaCheckTimer;
20
22
  private pendingUploads;
21
23
  private uploadDir;
22
24
  constructor(options: SessionHandlerOptions);
@@ -50,6 +52,9 @@ export declare class TmuxSessionHandler {
50
52
  * Get content type from file extension
51
53
  */
52
54
  private getContentType;
55
+ private startMetaTracking;
56
+ private stopMetaTracking;
57
+ private checkAndSendMeta;
53
58
  private handleKeys;
54
59
  private startScreenCapture;
55
60
  private stopScreenCapture;
@@ -39,6 +39,7 @@ const tmux = __importStar(require("./tmux"));
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  const sentry_1 = require("../sentry");
42
+ const debug_1 = require("./debug");
42
43
  // Capture intervals
43
44
  const CAPTURE_INTERVAL_ACTIVE_MS = 50;
44
45
  const CAPTURE_INTERVAL_IDLE_MS = 200;
@@ -46,6 +47,7 @@ const ACTIVE_THRESHOLD_MS = 2000;
46
47
  const FORCE_SEND_INTERVAL_MS = 10000;
47
48
  const USE_COMPRESSION = true;
48
49
  const MIN_COMPRESS_SIZE = 512;
50
+ const META_CHECK_INTERVAL_MS = 15000;
49
51
  class TmuxSessionHandler {
50
52
  constructor(options) {
51
53
  this.wsClient = null;
@@ -57,6 +59,9 @@ class TmuxSessionHandler {
57
59
  // Multi-pane tracking
58
60
  this.lastPaneIds = '';
59
61
  this.lastPaneScreens = new Map();
62
+ // Metadata tracking (cwd + git info)
63
+ this.currentMetaJson = '';
64
+ this.metaCheckTimer = null;
60
65
  // File upload handling
61
66
  this.pendingUploads = new Map();
62
67
  this.uploadDir = process.cwd(); // Default to current working directory
@@ -88,6 +93,7 @@ class TmuxSessionHandler {
88
93
  this.wsClient.on('connected', () => {
89
94
  console.log(`[${this.tmuxSession}] Connected to relay`);
90
95
  this.startScreenCapture();
96
+ this.startMetaTracking();
91
97
  });
92
98
  this.wsClient.on('disconnected', ({ code, reason }) => {
93
99
  console.log(`[${this.tmuxSession}] Disconnected: code=${code}, reason=${reason}`);
@@ -294,6 +300,43 @@ class TmuxSessionHandler {
294
300
  };
295
301
  return types[ext] || 'application/octet-stream';
296
302
  }
303
+ startMetaTracking() {
304
+ this.checkAndSendMeta();
305
+ this.metaCheckTimer = setInterval(() => this.checkAndSendMeta(), META_CHECK_INTERVAL_MS);
306
+ }
307
+ stopMetaTracking() {
308
+ if (this.metaCheckTimer) {
309
+ clearInterval(this.metaCheckTimer);
310
+ this.metaCheckTimer = null;
311
+ }
312
+ }
313
+ checkAndSendMeta() {
314
+ try {
315
+ const cwd = tmux.getPaneCwd(this.tmuxSession);
316
+ if (!cwd)
317
+ return;
318
+ const gitInfo = tmux.getGitInfo(cwd);
319
+ const meta = { cwd };
320
+ if (gitInfo?.branch)
321
+ meta.gitBranch = gitInfo.branch;
322
+ if (gitInfo?.remote)
323
+ meta.gitRemote = gitInfo.remote;
324
+ if (gitInfo?.repo)
325
+ meta.gitRepo = gitInfo.repo;
326
+ const json = JSON.stringify(meta);
327
+ (0, debug_1.debugLog)(this.tmuxSession, 'Meta check:', { cwd, gitInfo });
328
+ const changed = json !== this.currentMetaJson;
329
+ (0, debug_1.debugLog)(this.tmuxSession, changed ? 'Meta CHANGED, sending' : 'Meta unchanged, skip');
330
+ if (changed) {
331
+ this.currentMetaJson = json;
332
+ this.wsClient?.sendSessionMeta(meta);
333
+ console.log(`[${this.tmuxSession}] Session meta updated: ${json}`);
334
+ }
335
+ }
336
+ catch (error) {
337
+ // Silently ignore meta check errors
338
+ }
339
+ }
297
340
  handleKeys(keys, paneId) {
298
341
  const target = paneId || this.tmuxSession;
299
342
  tmux.sendKeys(target, keys, false);
@@ -405,6 +448,7 @@ class TmuxSessionHandler {
405
448
  console.log(`[${this.tmuxSession}] Stopping`);
406
449
  this.running = false;
407
450
  this.stopScreenCapture();
451
+ this.stopMetaTracking();
408
452
  if (this.wsClient) {
409
453
  this.wsClient.destroy();
410
454
  this.wsClient = null;
@@ -56,4 +56,17 @@ export declare function listPanes(sessionName: string): PaneData[] | null;
56
56
  * Capture a specific pane by its pane ID (e.g., %0, %1)
57
57
  */
58
58
  export declare function capturePaneById(sessionName: string, paneId: string): string | null;
59
+ /**
60
+ * Git info for a directory
61
+ */
62
+ export interface GitInfo {
63
+ branch: string | null;
64
+ remote: string | null;
65
+ repo: string | null;
66
+ }
67
+ /**
68
+ * Detect git info (branch, remote, repo) for a given directory.
69
+ * Returns null if the directory is not inside a git work tree.
70
+ */
71
+ export declare function getGitInfo(cwd: string): GitInfo | null;
59
72
  export type { PaneData } from './tmux-executor';
@@ -14,7 +14,10 @@ exports.getActivePane = getActivePane;
14
14
  exports.getPaneCwd = getPaneCwd;
15
15
  exports.listPanes = listPanes;
16
16
  exports.capturePaneById = capturePaneById;
17
+ exports.getGitInfo = getGitInfo;
17
18
  const tmux_executor_1 = require("./tmux-executor");
19
+ const child_process_1 = require("child_process");
20
+ const debug_1 = require("./debug");
18
21
  // Lazy-initialized executor (created on first use)
19
22
  let executor = null;
20
23
  /**
@@ -171,3 +174,30 @@ function listPanes(sessionName) {
171
174
  function capturePaneById(sessionName, paneId) {
172
175
  return getExecutor().capturePaneById(sessionName, paneId);
173
176
  }
177
+ /**
178
+ * Detect git info (branch, remote, repo) for a given directory.
179
+ * Returns null if the directory is not inside a git work tree.
180
+ */
181
+ function getGitInfo(cwd) {
182
+ try {
183
+ (0, child_process_1.execSync)('git rev-parse --is-inside-work-tree', { cwd, stdio: 'pipe', timeout: 3000 });
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ const run = (cmd) => {
189
+ try {
190
+ return (0, child_process_1.execSync)(cmd, { cwd, encoding: 'utf-8', stdio: 'pipe', timeout: 3000 }).trim();
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ };
196
+ const branch = run('git rev-parse --abbrev-ref HEAD');
197
+ const remote = run('git config --get remote.origin.url');
198
+ const repo = remote
199
+ ? remote.replace(/.*github\.com[:/]/, '').replace(/\.git$/, '')
200
+ : null;
201
+ (0, debug_1.debugLog)('git', `cwd=${cwd}, branch=${branch}, remote=${remote}, repo=${repo}`);
202
+ return { branch, remote, repo };
203
+ }
@@ -9,6 +9,21 @@ export interface ApiConfig {
9
9
  agentId?: string;
10
10
  exec?: ExecConfig;
11
11
  llm?: LlmConfig;
12
+ capabilities?: CapabilitiesConfig;
13
+ }
14
+ /**
15
+ * Per-capability consent config for the CLI agent.
16
+ * Each capability can be:
17
+ * true — auto-grant when SDK requests it
18
+ * false — auto-deny
19
+ * 'ask' — deny with a log notice (future: interactive prompt)
20
+ */
21
+ export interface CapabilitiesConfig {
22
+ exec?: boolean | 'ask';
23
+ exec_cwd?: boolean | 'ask';
24
+ llm_chat?: boolean | 'ask';
25
+ send_keys?: boolean | 'ask';
26
+ list_sessions?: boolean | 'ask';
12
27
  }
13
28
  export interface ExecConfig {
14
29
  enabled: boolean;
@@ -37,6 +52,12 @@ export interface TmuxSession {
37
52
  created?: string;
38
53
  attached: boolean;
39
54
  }
55
+ export interface SessionMetadata {
56
+ cwd: string | null;
57
+ gitBranch: string | null;
58
+ gitRemote: string | null;
59
+ gitRepo: string | null;
60
+ }
40
61
  export interface ExecResult {
41
62
  exitCode: number;
42
63
  stdout: string;
@@ -33,6 +33,7 @@ export declare class RelayWebSocketClient extends EventEmitter {
33
33
  sendScreenWithMeta(data: Buffer, meta: Record<string, string>): boolean;
34
34
  sendScreenCompressed(data: Buffer): boolean;
35
35
  sendScreenCompressedWithMeta(data: Buffer, meta: Record<string, string>): boolean;
36
+ sendSessionMeta(meta: Record<string, string>): boolean;
36
37
  sendPaneLayout(panes: Array<{
37
38
  id: string;
38
39
  index: number;
@@ -40,6 +40,7 @@ exports.RelayWebSocketClient = void 0;
40
40
  const ws_1 = __importDefault(require("ws"));
41
41
  const events_1 = require("events");
42
42
  const zlib = __importStar(require("zlib"));
43
+ const debug_1 = require("./debug");
43
44
  const MAX_RECONNECT_ATTEMPTS = 5;
44
45
  const BASE_RECONNECT_DELAY_MS = 2000;
45
46
  const MAX_RECONNECT_DELAY_MS = 60000;
@@ -116,6 +117,7 @@ class RelayWebSocketClient extends events_1.EventEmitter {
116
117
  });
117
118
  }
118
119
  handleMessage(message) {
120
+ (0, debug_1.debugLog)('WS:IN', message.type, message.session);
119
121
  switch (message.type) {
120
122
  case 'keys':
121
123
  if (message.session === this.sessionId && message.payload) {
@@ -232,6 +234,8 @@ class RelayWebSocketClient extends events_1.EventEmitter {
232
234
  return false;
233
235
  }
234
236
  try {
237
+ (0, debug_1.debugLog)('WS:OUT', message.type, message.session, message.type === 'sessionMeta' ? JSON.stringify(message.meta) :
238
+ message.payload ? `payload=${message.payload.length}bytes` : '');
235
239
  this.ws.send(JSON.stringify(message));
236
240
  return true;
237
241
  }
@@ -291,6 +295,15 @@ class RelayWebSocketClient extends events_1.EventEmitter {
291
295
  return this.sendScreenWithMeta(data, meta);
292
296
  }
293
297
  }
298
+ sendSessionMeta(meta) {
299
+ if (!this.isConnected)
300
+ return false;
301
+ return this.send({
302
+ type: 'sessionMeta',
303
+ session: this.sessionId,
304
+ meta
305
+ });
306
+ }
294
307
  sendPaneLayout(panes) {
295
308
  if (!this.isConnected)
296
309
  return false;
@@ -1,5 +1,6 @@
1
1
  interface AgentOptions {
2
2
  config?: string;
3
+ debug?: boolean;
3
4
  }
4
5
  export declare function startAgent(options: AgentOptions): Promise<void>;
5
6
  export {};
@@ -7,8 +7,13 @@ exports.startAgent = startAgent;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const runner_1 = require("../agent/runner");
9
9
  const sentry_1 = require("../sentry");
10
+ const debug_1 = require("../agent/debug");
10
11
  async function startAgent(options) {
11
12
  try {
13
+ if (options.debug) {
14
+ (0, debug_1.setDebug)(true);
15
+ console.log(chalk_1.default.yellow('[DEBUG] Debug mode enabled'));
16
+ }
12
17
  const config = runner_1.AgentRunner.loadConfig(options.config);
13
18
  const runner = new runner_1.AgentRunner(config);
14
19
  await runner.start();
package/dist/index.js CHANGED
@@ -209,6 +209,7 @@ program
209
209
  .command('agent')
210
210
  .description('Start the SessionCast agent')
211
211
  .option('-c, --config <path>', 'Path to config file')
212
+ .option('-d, --debug', 'Enable debug logging')
212
213
  .action(agent_1.startAgent);
213
214
  // Help examples
214
215
  program.on('--help', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessioncast-cli",
3
- "version": "2.0.9",
3
+ "version": "2.1.0",
4
4
  "description": "SessionCast CLI - Control your agents from anywhere",
5
5
  "main": "dist/index.js",
6
6
  "bin": {