@volter/supercode-teams 0.2.173 → 0.2.175
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/bin/fleet.mjs +3 -2
- package/bin/workspace.mjs +3 -3
- package/fleet.mjs +19 -0
- package/machine.mjs +9 -4
- package/package.json +1 -1
- package/server/harness-routes.mjs +11 -2
package/bin/fleet.mjs
CHANGED
|
@@ -96,8 +96,9 @@ async function main() {
|
|
|
96
96
|
cwd: flags.cwd,
|
|
97
97
|
initialInput: flags.input,
|
|
98
98
|
name: flags.name,
|
|
99
|
-
// `open` attaching this terminal to the pane
|
|
100
|
-
|
|
99
|
+
// The launch's window choice, when its caller states one: `open` attaching this terminal to the pane asks
|
|
100
|
+
// for none, and the board's dispatcher states its workflow's choice on every launch.
|
|
101
|
+
...(flags.popup === 'false' || flags.popup === 'true' ? { popup: flags.popup === 'true' } : {}),
|
|
101
102
|
// Who starts the pane (the session or board supercode resolved; unset for a person), and whether the pane
|
|
102
103
|
// runs its one verb as that caller (`close`/`stop` on another machine).
|
|
103
104
|
...(launcher() ? { caller: launcher() } : {}),
|
package/bin/workspace.mjs
CHANGED
|
@@ -793,7 +793,7 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
793
793
|
if (flags.install || (!flags.foreground && process.stdin.isTTY && process.stdout.isTTY)) {
|
|
794
794
|
const serviceBin = process.env.SUPERCODE_BIN;
|
|
795
795
|
if (!serviceBin) unsupported('--install requires invocation through the native supercode CLI');
|
|
796
|
-
const serviceEnv = { ...process.env,
|
|
796
|
+
const serviceEnv = { ...process.env, SUPERCODE_TEAMS_ENTRY: fileURLToPath(new URL('./teams.mjs', import.meta.url)) };
|
|
797
797
|
// Refuse source-backed installs before Windows stops the running connector.
|
|
798
798
|
try {
|
|
799
799
|
execFileSync(serviceBin, ['__teams-connector-service', 'validate', contextName, path, context.server_id, context.team_id], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: serviceEnv });
|
|
@@ -838,9 +838,9 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
838
838
|
// Followed (live) sessions follow and load through one of their own, so a watched session never waits behind a pass.
|
|
839
839
|
const liveCore = new SupercodeHarnessClient({ command: binary, args: ['harness', 'serve'] });
|
|
840
840
|
const connector = new TeamsConnector({ server: context.origin, teamId: context.team_id, machineId: saved.machine.id, sourceId: saved.source.id, credential: saved.credential.token, stateDir: state.directory, core, largeCore, slowCore, liveCore, hostKeyId });
|
|
841
|
-
// A connected machine is a person's: each pane it launches opens a window on its screen
|
|
841
|
+
// A connected machine is a person's: each pane it launches opens a window on its screen.
|
|
842
842
|
const machineEvents = new MachineEvents({home:daemonHome,server:context.origin,teamId:context.team_id,credential:saved.credential.token,machineName:saved.machine.name,binary});
|
|
843
|
-
const machine = new TeamsMachine({ home: daemonHome, supercode: binary,
|
|
843
|
+
const machine = new TeamsMachine({ home: daemonHome, supercode: binary, ...(flags.tmux_socket ? { tmuxSocket: String(flags.tmux_socket) } : {}), link: { server: context.origin, teamId: context.team_id, machineId: saved.machine.id, credential: saved.credential.token, incarnationId: connector.state.incarnation_id, configuration: () => connector.configuration() }, onTurn: (turn) => connector.noteTurn(turn), onTurnWithdrawn: (turn) => connector.withdrawTurn(turn) });
|
|
844
844
|
machine.machineEvents = machineEvents;
|
|
845
845
|
machine.on('link', (event) => { if (event.event === 'replaced') process.stderr.write(`supercode teams: ${event.error}. Two connectors were running for this machine; stop one of them (\`supercode teams disconnect\` where it should not run).\n`); });
|
|
846
846
|
// The link's state for the watch after start: down until it is up, down again (since when) when it drops. A
|
package/fleet.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// `open --on`, and `harness drive --on`. A machine named with `--on` is an
|
|
3
3
|
// enrolled machine of the selected Teams context, reached through its server;
|
|
4
4
|
// without `--on` the verb acts on this OS user's own daemon.
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
5
6
|
import { resolve } from 'node:path';
|
|
6
7
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
7
8
|
import { harnessTransport, nodeRpc, openRemoteMachine } from './client.mjs';
|
|
@@ -52,6 +53,9 @@ export async function fleetDiscover({ context = null, machines = null, params =
|
|
|
52
53
|
const { client, context: selected } = fromContext({ supercodeHome, name, fetch: globalThis.fetch });
|
|
53
54
|
const token = contexts.credential(selected.credential_ref).token;
|
|
54
55
|
const listed = (await client.team(selected.team_id).machines.list()).items.filter((item) => item.enrollment === 'active');
|
|
56
|
+
// This machine's own sessions are read here, as `discover` reads them, not through the server and back
|
|
57
|
+
// into this machine's daemon.
|
|
58
|
+
const here = contexts.connector(selected.server_id, selected.team_id).read()?.machine?.id ?? null;
|
|
55
59
|
const targets = machines && machines.length
|
|
56
60
|
? machines.map((wanted) => listed.find((item) => item.name === wanted || item.id === wanted) ?? { name: wanted, id: null })
|
|
57
61
|
: listed;
|
|
@@ -60,6 +64,10 @@ export async function fleetDiscover({ context = null, machines = null, params =
|
|
|
60
64
|
// A machine the server reports offline is not asked (asking only waits for a link that is not coming); it is
|
|
61
65
|
// named as offline, not as a failure, and only when it was named or seen in the last day.
|
|
62
66
|
if (machine.connection === 'offline') return { name: machine.name, offline: true, lastSeenAt: machine.last_seen_at ?? null };
|
|
67
|
+
if (machine.id === here) {
|
|
68
|
+
try { return { name: machine.name, sessions: await discoverHere(params, env) }; }
|
|
69
|
+
catch (error) { return { name: machine.name, error: error.message ?? String(error) }; }
|
|
70
|
+
}
|
|
63
71
|
let channel = null;
|
|
64
72
|
try {
|
|
65
73
|
channel = await openRemoteMachine({ origin: selected.origin, teamId: selected.team_id, machineId: machine.id, token });
|
|
@@ -83,3 +91,14 @@ export async function fleetDiscover({ context = null, machines = null, params =
|
|
|
83
91
|
}
|
|
84
92
|
return { sessions, unreachable, ...(offline.length ? { offline } : {}) };
|
|
85
93
|
}
|
|
94
|
+
|
|
95
|
+
/** `harness.v1.sessions.discover` on this machine: the local `supercode discover` page. */
|
|
96
|
+
function discoverHere(params, env) {
|
|
97
|
+
const args = ['discover', '--context', 'local', '--page'];
|
|
98
|
+
if (params.query) args.push('--query', String(params.query));
|
|
99
|
+
if (params.limit) args.push('--limit', String(params.limit));
|
|
100
|
+
if (params.workspace) args.push('--workspace', String(params.workspace));
|
|
101
|
+
for (const harness of params.harnesses ?? []) args.push('--harness', String(harness));
|
|
102
|
+
return new Promise((done, fail) => execFile(env.SUPERCODE_BIN || 'supercode', args, { env, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, timeout: 60_000 },
|
|
103
|
+
(error, stdout, stderr) => error ? fail(new Error((stderr || error.message).trim().split('\n').at(-1))) : done(JSON.parse(stdout).sessions ?? [])));
|
|
104
|
+
}
|
package/machine.mjs
CHANGED
|
@@ -161,15 +161,13 @@ export class TeamsMachine extends MachineCore {
|
|
|
161
161
|
* server link; `configuration()` resolves the machine's current configuration from the server
|
|
162
162
|
* (custodian and machine grants). Without it the daemon serves its local door only.
|
|
163
163
|
*/
|
|
164
|
-
constructor({ home = teamsHome(), supercode = 'supercode', tmuxSocket, floor = null, limits = null, env = process.env, link = null,
|
|
164
|
+
constructor({ home = teamsHome(), supercode = 'supercode', tmuxSocket, floor = null, limits = null, env = process.env, link = null, onTurn = null, onTurnWithdrawn = null } = {}) {
|
|
165
165
|
super({ home, floor, limits, env, link });
|
|
166
166
|
this.supercodeBin = supercode;
|
|
167
167
|
// Told of each turn a Room member typed here (the connector labels the captured message with who typed it).
|
|
168
168
|
this.onTurn = onTurn;
|
|
169
169
|
this.onTurnWithdrawn = onTurnWithdrawn;
|
|
170
170
|
this.tmuxSocket = tmuxSocket ?? null;
|
|
171
|
-
// Each pane launched here opens a terminal window on this machine's screen (popupWindow).
|
|
172
|
-
this.popups = Boolean(popups);
|
|
173
171
|
this.launches = new Launches(this.p.launches);
|
|
174
172
|
// Windows has no tmux: its panes live in the machine's pane server, a process of their own that outlives this
|
|
175
173
|
// daemon (a restart or an upgrade never ends a pane), as tmux panes live in tmux's server.
|
|
@@ -913,6 +911,13 @@ export class TeamsMachine extends MachineCore {
|
|
|
913
911
|
queue.identity = saved.binding.identity; queue.host = this.host; queue.session = session;
|
|
914
912
|
this.terminalMailboxes.set(saved.recipient, queue);
|
|
915
913
|
}
|
|
914
|
+
// A queue whose pane has ended is not retried every second into a terminal that no longer
|
|
915
|
+
// exists. Its mail stays retained: the session's next pane takes it up.
|
|
916
|
+
for (const [recipient, queue] of this.terminalMailboxes) {
|
|
917
|
+
const binding = queue.state.binding;
|
|
918
|
+
const live = binding ? await this.#paneByContext(binding.context) : null;
|
|
919
|
+
if (!live || live.id !== binding.terminal) this.terminalMailboxes.delete(recipient);
|
|
920
|
+
}
|
|
916
921
|
await Promise.all([...this.terminalMailboxes.values()].filter(queue => !this.inputRepairs[queue.state.binding?.context] && !this.inputInFlight.has(queue.session?.id)).map(queue => queue.kick()));
|
|
917
922
|
}
|
|
918
923
|
|
|
@@ -1536,7 +1541,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
1536
1541
|
this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, ...executable, status, error: exit?.failed ? exit.how : undefined });
|
|
1537
1542
|
return { key, pane: contextKey, reused: false, status, exit: exit?.how ?? null };
|
|
1538
1543
|
}
|
|
1539
|
-
if (
|
|
1544
|
+
if (params.popup !== false) this.#popupWindow(contextKey, session);
|
|
1540
1545
|
if (params.notify) this.#noticeWhenEnded({ key, pane: contextKey, notify: params.notify, label: params.name ?? params.kind ?? params.program });
|
|
1541
1546
|
return { key, pane: contextKey, reused: false, status: 'ready' };
|
|
1542
1547
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.175",
|
|
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",
|
|
@@ -17,7 +17,7 @@ import { handleCatalogEvents, handleSessionEvents, sessionNames } from './event-
|
|
|
17
17
|
import { exact } from '@volter/teams/server/request-json';
|
|
18
18
|
import { MESSAGE_AT, OPEN_REVISION } from '@volter/teams/server/migrations';
|
|
19
19
|
import { delegatedRoomNoticeOf, delegatedSessionOf, looksDelegated } from '@volter/teams/server/delegated';
|
|
20
|
-
import { deliverSessionRoomNotice } from '@volter/teams/server/machine-link';
|
|
20
|
+
import { deliverSessionRoomNotice, sessionDeliveryResolution } from '@volter/teams/server/machine-link';
|
|
21
21
|
import { renderSourceCollectorConfig } from './otlp/collector-config.mjs';
|
|
22
22
|
import { attachmentActionRoute, createAttachment, liveGrantRoute, machineEffectRoute } from './effects.mjs';
|
|
23
23
|
import { exportSessionRoute, operationRoute } from './exports.mjs';
|
|
@@ -437,7 +437,16 @@ async function viewBindingRoute(api,req,res,auth,ref,requestId){
|
|
|
437
437
|
if(!['user','service'].includes(auth.kind))fail(403,'forbidden','credential cannot bind sessions');
|
|
438
438
|
const session=await api.store.authorizedSession({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref,now:api.clock.nowMs()});
|
|
439
439
|
const list=async()=>api.store.query('SELECT issuer,room,conversation,bound_by,created_at FROM session_view_bindings WHERE team_id=? AND session_id=? ORDER BY created_at',[auth.team_id,session.id]);
|
|
440
|
-
if(req.method==='GET'){
|
|
440
|
+
if(req.method==='GET'){
|
|
441
|
+
const bindings=await list();
|
|
442
|
+
// Native transport destinations are custody information, not an extra transcript-reader capability.
|
|
443
|
+
if(session.custodian_id===auth.principal_id&&!auth.installation_id)for(const binding of bindings){
|
|
444
|
+
const resolution=await sessionDeliveryResolution(api,auth.team_id,{sessionRef:session.id,...binding},true);
|
|
445
|
+
binding.delivery=resolution.target&&resolution.target.harness!==session.harness
|
|
446
|
+
? {...resolution,state:'unavailable',reason:'harness_mismatch'} : resolution;
|
|
447
|
+
}
|
|
448
|
+
send(res,200,result(api,auth,{session_id:session.id,harness:session.harness,bindings}),requestId);return;
|
|
449
|
+
}
|
|
441
450
|
if(session.custodian_id!==auth.principal_id)fail(403,'forbidden',"only the session's owner binds it to a view");
|
|
442
451
|
const body=await parseJson(req);exact(body,['issuer','room','conversation']);
|
|
443
452
|
if(typeof body.room!=='string'||!body.room||typeof body.conversation!=='string'||!body.conversation)fail(400,'invalid_request','room and conversation are required');
|