@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.
- package/connector/index.mjs +7 -1
- package/machine.mjs +59 -1
- package/package.json +1 -1
package/connector/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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",
|