@volter/supercode-teams 0.2.36 → 0.2.37

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.
@@ -151,7 +151,13 @@ export class TeamsConnector {
151
151
  if (running || this.abort.signal.aborted) return null;
152
152
  running = true;
153
153
  const run = (async () => { this.localRoots = await roots(); await pass(); })();
154
- void run.catch(error => { this.lastError = error; }).finally(() => { running = false; });
154
+ // A pass that fails is said in the connector's log (once per distinct failure): nothing syncs until it passes.
155
+ void run.catch(error => {
156
+ this.lastError = error;
157
+ const said = `${error?.code ?? ''} ${error?.message ?? error}`;
158
+ if (said !== this.lastPassFailure) process.stderr.write(`supercode teams: a sync pass failed: ${error?.message ?? error}\n`);
159
+ this.lastPassFailure = said;
160
+ }).then(() => { if (run && !this.lastError) this.lastPassFailure = null; }).finally(() => { running = false; });
155
161
  return run;
156
162
  };
157
163
  /** The first sync pass, for a caller that waits on it; the connector keeps running if it fails. */
package/machine.mjs CHANGED
@@ -68,6 +68,31 @@ const COMPOSER_RECHECK_MS = 400;
68
68
  * must have stopped changing for this long before the pane counts as ready. */
69
69
  const INITIAL_INPUT_SETTLE_MS = 1_000;
70
70
 
71
+ /**
72
+ * Claude asks whether to trust a folder it has not opened before, whatever its permission mode, and has no flag that
73
+ * skips the question; its default answer exits, taking the pane with it. A Claude this machine launches without
74
+ * prompts has its folder recorded as trusted in this machine's Claude config (`projects[<folder>]` in .claude.json,
75
+ * keyed as Claude keys it: the real path, with forward slashes on Windows), as a Codex launch has its trust passed on
76
+ * its command line. A whole new file is renamed over the old one, since Claude rewrites it while it runs.
77
+ */
78
+ function trustClaudeFolder(cwd) {
79
+ try {
80
+ const config = join(process.env.CLAUDE_CONFIG_DIR || homedir(), '.claude.json');
81
+ const real = realpathSync(cwd);
82
+ const folder = process.platform === 'win32' ? real.replace(/^\\\\\?\\/, '').replaceAll('\\', '/') : real;
83
+ let root = {};
84
+ try { root = JSON.parse(readFileSync(config, 'utf8')); } catch (error) { if (error.code !== 'ENOENT') return; }
85
+ if (!root || typeof root !== 'object' || Array.isArray(root)) return;
86
+ const projects = root.projects ??= {};
87
+ const project = projects[folder] ??= {};
88
+ if (project.hasTrustDialogAccepted === true) return;
89
+ project.hasTrustDialogAccepted = true;
90
+ const staged = `${config}.supercode-${process.pid}`;
91
+ writeFileSync(staged, JSON.stringify(root, null, 2));
92
+ renameSync(staged, config);
93
+ } catch { /* Claude asks as it always did */ }
94
+ }
95
+
71
96
  /** How long a launch watches its new pane before answering, so a program that ends at once is reported. */
72
97
  const LAUNCH_SETTLE_MS = 500;
73
98
 
@@ -353,9 +378,10 @@ export class TeamsMachine extends MachineCore {
353
378
  // receiver named rather than addressed needs `mail:*`.
354
379
  const { admitted, principal } = entry;
355
380
  const request = stream.params?.request;
381
+ if (request?.op === 'turn') return this.#serveTurn(entry, request, stream, accept, refuse);
356
382
  const to = typeof request?.to === 'string' ? request.to : '*';
357
383
  const target = to.startsWith('sc:') ? `mail:${to}` : 'mail:*';
358
- if (!request || !['send', 'file', 'list'].includes(request.op)) return refuse('mail needs a send, file or list request');
384
+ if (!request || !['send', 'file', 'list'].includes(request.op)) return refuse('mail needs a send, file, list or turn request');
359
385
  if (!holds(admitted, 'mail', target)) {
360
386
  this.log.append({ principal, cap: 'mail', target, outcome: 'refused' });
361
387
  return refuse(`no mail grant for ${target}`);
@@ -377,6 +403,37 @@ export class TeamsMachine extends MachineCore {
377
403
  child.stdin.end(JSON.stringify({ ...request, via_principal: principal ?? null }));
378
404
  }
379
405
 
406
+ #serveTurn(entry, request, stream, accept, refuse) {
407
+ // A person's line typed into one session here as that session's own user turn (a Room member in a Conversation
408
+ // bound to the session): `interact` on `session:<harness>/<id>`. Who typed it comes from the caller's claim
409
+ // (the name its workplace signed), never from the request.
410
+ const { admitted, principal } = entry;
411
+ // A caller the server admitted for one session (a Room member's token) names nothing: its grant names the session.
412
+ const granted = admitted?.class === 'operator' ? [] : (admitted?.grants ?? []).filter((grant) => grant.caps.includes('interact')).flatMap((grant) => grant.targets).filter((target) => target.startsWith('session:') && target !== 'session:*');
413
+ const named = typeof request.harness === 'string' && typeof request.session === 'string' ? `${request.harness}/${request.session}` : (granted.length === 1 ? granted[0].slice('session:'.length) : '');
414
+ const [harness, session] = named.includes('/') ? [named.slice(0, named.indexOf('/')), named.slice(named.indexOf('/') + 1)] : ['', ''];
415
+ const target = `session:${harness}/${session}`;
416
+ if (!harness || !session || typeof request.text !== 'string' || !request.text.trim()) return refuse('a turn needs harness, session and text');
417
+ if (!holds(admitted, 'interact', target)) {
418
+ this.log.append({ principal, cap: 'interact', target, outcome: 'refused' });
419
+ return refuse(`no interact grant for ${target}`);
420
+ }
421
+ accept();
422
+ const child = spawn(this.supercodeBin, ['message', 'remote-deliver'], { stdio: ['pipe', 'pipe', 'pipe'], env: this.env });
423
+ let out = '';
424
+ let err = '';
425
+ child.stdout.on('data', (chunk) => { out += chunk; });
426
+ child.stderr.on('data', (chunk) => { err += chunk; });
427
+ child.on('error', (error) => { err += error.message; });
428
+ child.on('close', (code) => {
429
+ this.log.append({ principal, cap: 'interact', target, outcome: code === 0 ? 'turn' : 'failed', detail: err.trim().slice(0, 200) || undefined });
430
+ if (stream.closed) return;
431
+ stream.write(out.trim() || JSON.stringify({ code: 1, text: err.trim() || `remote-deliver exited ${code}` }));
432
+ stream.close('delivered');
433
+ });
434
+ child.stdin.end(JSON.stringify({ op: 'turn', harness, session, text: request.text, from_name: entry.name ?? principal ?? 'a Room member', via: 'room' }));
435
+ }
436
+
380
437
  // ---- terminal ------------------------------------------------------------
381
438
 
382
439
  async #attachTerminal(entry, stream, accept, refuse) {
@@ -989,6 +1046,7 @@ export class TeamsMachine extends MachineCore {
989
1046
  }
990
1047
  const contextKey = params.pane ?? `p_${randomBytes(9).toString('base64url')}`;
991
1048
  this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, status: 'issued' });
1049
+ if (params.kind === 'claude-code' && params.cwd && (params.arguments ?? []).includes('--dangerously-skip-permissions')) trustClaudeFolder(params.cwd);
992
1050
  let session;
993
1051
  try {
994
1052
  session = await this.host.createSession({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.36",
3
+ "version": "0.2.37",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",