@volter/supercode-teams 0.3.1 → 0.3.3
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/teams.mjs +44 -3
- package/bin/workspace.mjs +5 -0
- package/machine.mjs +80 -3
- package/mail-history.mjs +1 -1
- package/package.json +2 -1
package/bin/teams.mjs
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
// attention ls | rank | request <pane> [--urgency now|soon|whenever] [--reason text] | clear <pane>
|
|
21
21
|
// watch [--interval ms] [--for ms]
|
|
22
22
|
// launches get <key> | cancel <key> | list operators: keyed receipts; cancel only unissued keys
|
|
23
|
+
// health [status|features|alarms] [--json] [--all] the machine's vitals (the machine-health pack)
|
|
24
|
+
// health subscribe|unsubscribe <address> operators: alarm mail for a session or board address
|
|
25
|
+
// health run | once | config this machine only: the pack's own probe verbs
|
|
23
26
|
// rpc <harness.v1 method> [json-params] operators: one harness.v1 request
|
|
24
27
|
// log verify
|
|
25
28
|
//
|
|
@@ -28,6 +31,7 @@
|
|
|
28
31
|
// CLI in ./workspace.mjs.
|
|
29
32
|
import { createWriteStream, readFileSync, writeFileSync } from 'node:fs';
|
|
30
33
|
import { spawnSync } from 'node:child_process';
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
35
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
32
36
|
|
|
33
37
|
// A Windows scheduled task keeps no output (launchd and systemd name log files), so its unit names one here.
|
|
@@ -45,9 +49,9 @@ import { moveWorkspace } from '../workspace-move.mjs';
|
|
|
45
49
|
import { teamsHome, paths, readJson, AccountLog, parseHostPort } from '@volter/teams/home';
|
|
46
50
|
import { NODE_PROTOCOL } from '@volter/teams/channel';
|
|
47
51
|
|
|
48
|
-
const MACHINE_VERBS = new Set(['machine', 'describe', 'panes', 'input', 'suspend', 'resume', 'files', 'ports', 'port', 'browser', 'stream', 'jobs', 'workspace', 'title', 'attention', 'watch', 'rpc', 'launches', 'log', 'mail']);
|
|
52
|
+
const MACHINE_VERBS = new Set(['machine', 'describe', 'panes', 'input', 'suspend', 'resume', 'files', 'ports', 'port', 'browser', 'stream', 'jobs', 'workspace', 'title', 'attention', 'watch', 'rpc', 'launches', 'log', 'mail', 'health']);
|
|
49
53
|
// Every flag a machine verb reads; any other is refused rather than silently ignored.
|
|
50
|
-
const KNOWN_FLAGS = new Set(['home', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path', 'repo', 'branch', 'solo', 'card', 'create', 'presence-only', 'clear-draft', 'message-ids']);
|
|
54
|
+
const KNOWN_FLAGS = new Set(['home', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path', 'repo', 'branch', 'solo', 'card', 'create', 'presence-only', 'clear-draft', 'message-ids', 'json', 'all']);
|
|
51
55
|
|
|
52
56
|
const argv = process.argv.slice(2);
|
|
53
57
|
const flags = {};
|
|
@@ -65,7 +69,7 @@ for (let i = 0; i < argv.length; i += 1) {
|
|
|
65
69
|
}
|
|
66
70
|
const [verb, sub, ...rest] = positional;
|
|
67
71
|
|
|
68
|
-
const MACHINE_HELP = 'machine verbs: machine start|status, describe, panes, input, suspend, resume, files, ports, port, stream, jobs, workspace, title, attention, watch, rpc, launches, log, browser (add --machine NAME for an enrolled machine)';
|
|
72
|
+
const MACHINE_HELP = 'machine verbs: machine start|status, describe, panes, input, suspend, resume, files, ports, port, stream, jobs, workspace, title, attention, watch, rpc, launches, log, health, browser (add --machine NAME for an enrolled machine)';
|
|
69
73
|
|
|
70
74
|
if (verb === undefined || verb === 'help') {
|
|
71
75
|
process.stdout.write(`supercode teams: ${MACHINE_HELP}\n`);
|
|
@@ -133,6 +137,15 @@ async function withNode(fn) {
|
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
|
|
140
|
+
/** The machine-health pack's CLI: an explicit path, the installed package, else a source checkout's sibling. */
|
|
141
|
+
async function healthPackEntry() {
|
|
142
|
+
const { existsSync } = await import('node:fs');
|
|
143
|
+
if (process.env.SUPERCODE_HEALTH_ENTRY) return existsSync(process.env.SUPERCODE_HEALTH_ENTRY) ? process.env.SUPERCODE_HEALTH_ENTRY : null;
|
|
144
|
+
try { const found = fileURLToPath(import.meta.resolve('@volter/supercode-health/bin')); if (existsSync(found)) return found; } catch { /* not installed */ }
|
|
145
|
+
const sibling = fileURLToPath(new URL('../../health/bin/health.mjs', import.meta.url));
|
|
146
|
+
return existsSync(sibling) ? sibling : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
136
149
|
async function main() {
|
|
137
150
|
switch (verb) {
|
|
138
151
|
case 'machine': {
|
|
@@ -329,6 +342,34 @@ async function main() {
|
|
|
329
342
|
die('launches get <key> | cancel <key> | list [--machine NAME]');
|
|
330
343
|
return;
|
|
331
344
|
}
|
|
345
|
+
case 'health': {
|
|
346
|
+
// This machine's vitals are read from the pack's own files, not through the daemon: the reading has to
|
|
347
|
+
// work when the daemon is what is sick. `--machine NAME` reads an enrolled machine through its doors.
|
|
348
|
+
const local = !flags.machine;
|
|
349
|
+
if (local || ['run', 'once', 'config'].includes(sub)) {
|
|
350
|
+
if (!local) die(`health ${sub} runs on the machine itself; read another machine with health status --machine NAME`);
|
|
351
|
+
const entry = await healthPackEntry();
|
|
352
|
+
if (!entry) die('the machine-health pack (@volter/supercode-health) is not installed; set SUPERCODE_HEALTH_ENTRY to its bin/health.mjs', 1);
|
|
353
|
+
const r = spawnSync(process.execPath, [entry, ...(sub ? [sub] : []), ...rest, ...(flags.json ? ['--json'] : []), ...(flags.all ? ['--all'] : [])], { stdio: 'inherit' });
|
|
354
|
+
process.exit(r.status ?? 1);
|
|
355
|
+
}
|
|
356
|
+
if (sub === 'subscribe' || sub === 'unsubscribe') {
|
|
357
|
+
if (!rest[0]) die(`health ${sub} <sc:machine:harness:id> --machine NAME`);
|
|
358
|
+
return withNode(async (rpc) => out(await rpc.call(`health.${sub}`, { address: rest[0] })));
|
|
359
|
+
}
|
|
360
|
+
if (sub && !['status', 'features', 'alarms'].includes(sub)) die('health [status|features|alarms] [--machine NAME] | health subscribe|unsubscribe <address> | health run|once|config');
|
|
361
|
+
return withNode(async (rpc) => {
|
|
362
|
+
const answer = await rpc.call('health.read', {});
|
|
363
|
+
const s = answer.snapshot;
|
|
364
|
+
const entry = await healthPackEntry();
|
|
365
|
+
const fmt = entry ? await import(new URL('../lib/format.mjs', pathToFileURL(entry)).href).catch(() => null) : null;
|
|
366
|
+
if (flags.json || !fmt) { out(sub === 'features' ? s?.features ?? null : sub === 'alarms' ? s?.alarms ?? null : answer); return; }
|
|
367
|
+
if (sub === 'features') { out(s ? fmt.formatFeatures(s.features) : 'no reading yet'); return; }
|
|
368
|
+
if (sub === 'alarms') { out(s?.alarms?.length ? s.alarms.map((a) => `${a.level.toUpperCase()} ${a.key}: ${a.summary} (since ${a.since})`).join('\n') : 'no alarms raised'); return; }
|
|
369
|
+
out(fmt.formatStatus(s, { all: Boolean(flags.all) }));
|
|
370
|
+
if (!answer.probe?.running) out(`probe: not run by that machine's daemon (${answer.probe?.reason ?? 'unknown'})`);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
332
373
|
case 'rpc': {
|
|
333
374
|
const channel = await machineChannel();
|
|
334
375
|
const client = new SupercodeHarnessClient({ transport: harnessTransport(channel) });
|
package/bin/workspace.mjs
CHANGED
|
@@ -485,6 +485,9 @@ async function sessionsCommand(sub, command) {
|
|
|
485
485
|
if (sub === 'url') { only(flags, ['json', 'help', 'context']); requireArgs(rest, 1, 1, 'sessions url REF'); const descriptor = await session.descriptor(); return output({ version: 1, context: scope(context), data: { url: `${context.origin}/teams/${context.team_id}/sessions/${descriptor.data.id}` } }); }
|
|
486
486
|
// A session view (RH2 ADR 0008): its owner lets a workplace Room's Conversation read this session with the tokens that
|
|
487
487
|
// workplace signs. `bind` records it and prints what the workplace's own bind door takes; `unbind` removes it.
|
|
488
|
+
// The custodian's read of a session's view bindings, each with where a Room notice to it would be delivered now
|
|
489
|
+
// (ready, stopped and resumable, ended, unknown liveness, or unavailable with the failing predicate).
|
|
490
|
+
if (sub === 'bindings') { only(flags, ['json', 'help', 'context']); requireArgs(rest, 1, 1, 'sessions bindings REF'); return output(await command.client.request('GET', `${session.path}/view-bindings`)); }
|
|
488
491
|
if (sub === 'bind' || sub === 'unbind') {
|
|
489
492
|
only(flags, ['json', 'help', 'context', 'room', 'agent', 'conversation', 'rh2']); requireArgs(rest, 1, 1, `sessions ${sub} REF --room ROOM --agent PRINCIPAL --rh2 ORIGIN`);
|
|
490
493
|
if (!flags.room || (!flags.agent && !flags.conversation) || (flags.agent && flags.conversation) || !flags.rh2) usage(`sessions ${sub} needs --room, --agent (or a legacy --conversation), and --rh2 (the workplace's origin)`);
|
|
@@ -994,6 +997,8 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
994
997
|
const machineEvents = new MachineEvents({home:daemonHome,server:context.origin,teamId:context.team_id,credential:saved.credential.token,machineName:saved.machine.name,binary});
|
|
995
998
|
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) });
|
|
996
999
|
machine.machineEvents = machineEvents;
|
|
1000
|
+
// The machine-health probe mails as this enrolled machine (`sc:<machine>:operator:machine-health`).
|
|
1001
|
+
machine.healthMachine = saved.machine.name;
|
|
997
1002
|
connector.closeAgentSession=async id=>{
|
|
998
1003
|
const channel=await openLocalMachine(paths(daemonHome).socket);
|
|
999
1004
|
try{const rpc=await nodeRpc(channel);return await rpc.call('harness.v1.agents.closeSession',{closure:id},{timeoutMs:60000});}
|
package/machine.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { Workspaces, inspectCheckout } from './workspaces.mjs';
|
|
|
35
35
|
import { WorkspaceContexts, fromContext } from '@volter/teams/context';
|
|
36
36
|
import { TeamsClient } from '@volter/teams/http-client';
|
|
37
37
|
import { supercodeHome } from '@volter/teams/home';
|
|
38
|
-
import { readMailHistory } from './mail-history.mjs';
|
|
38
|
+
import { readMailHistory, machineName } from './mail-history.mjs';
|
|
39
39
|
import { randomBytes, createHash } from 'node:crypto';
|
|
40
40
|
import { WebSocket } from 'ws';
|
|
41
41
|
import { TmuxTerminalHost, classifyAttentionSignal, composerHoldsDraft, composerReadyForInput } from '@volter/supercode-terminal';
|
|
@@ -259,10 +259,45 @@ export class TeamsMachine extends MachineCore {
|
|
|
259
259
|
|
|
260
260
|
async afterListen() {
|
|
261
261
|
this.#startMailWatch();
|
|
262
|
+
this.#startHealth();
|
|
262
263
|
if(Object.keys(this.inputRepairs).length)this.#watchInputRepairs();
|
|
263
264
|
}
|
|
264
265
|
|
|
266
|
+
// ---- the machine-health pack (docs/adr/0006-machine-health-pack.md) -------
|
|
267
|
+
// An enrolled machine runs the optional pack's probe as a child, so every connected machine is measured.
|
|
268
|
+
// The pack holds one probe per OS user (exit 3: another holds it; retried a minute later in case that one
|
|
269
|
+
// stops). SUPERCODE_HEALTH=0 turns it off. The probe only reads and mails; this daemon serves its readings.
|
|
270
|
+
#startHealth() {
|
|
271
|
+
this.healthStopped = false;
|
|
272
|
+
if (!this.link || (this.env.SUPERCODE_HEALTH ?? '1') === '0') { this.healthRun = { running: false, reason: this.link ? 'turned off (SUPERCODE_HEALTH=0)' : 'not an enrolled connector' }; return; }
|
|
273
|
+
const entry = healthEntry(this.env);
|
|
274
|
+
if (!entry) { this.healthRun = { running: false, reason: 'the machine-health pack (@volter/supercode-health) is not installed; set SUPERCODE_HEALTH_ENTRY to its bin/health.mjs' }; return; }
|
|
275
|
+
const start = () => {
|
|
276
|
+
if (this.healthStopped) return;
|
|
277
|
+
const child = spawn(process.execPath, [entry, 'run', ...(this.healthMachine ? ['--machine', this.healthMachine] : [])], { env: { ...this.env, SUPERCODE_BIN: this.supercodeBin }, stdio: 'ignore', windowsHide: true });
|
|
278
|
+
this.healthChild = child;
|
|
279
|
+
this.healthRun = { running: true, pid: child.pid, entry, since: Date.now() };
|
|
280
|
+
child.once('error', (error) => { this.healthRun = { running: false, reason: error.message, entry }; });
|
|
281
|
+
child.once('exit', (code, signal) => {
|
|
282
|
+
if (this.healthChild === child) this.healthChild = null;
|
|
283
|
+
if (this.healthStopped) return;
|
|
284
|
+
const held = code === 3;
|
|
285
|
+
this.healthBackoff = held ? 60_000 : Math.min(300_000, (this.healthBackoff ?? 2500) * 2);
|
|
286
|
+
this.healthRun = { running: false, reason: held ? 'another probe holds this user\'s pidfile' : `exited (${signal ?? code}); restarting`, entry, exitedAt: Date.now() };
|
|
287
|
+
this.healthTimer = setTimeout(start, this.healthBackoff); this.healthTimer.unref?.();
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
start();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#stopHealth() {
|
|
294
|
+
this.healthStopped = true;
|
|
295
|
+
clearTimeout(this.healthTimer);
|
|
296
|
+
this.healthChild?.kill('SIGTERM');
|
|
297
|
+
}
|
|
298
|
+
|
|
265
299
|
async beforeClose() {
|
|
300
|
+
this.#stopHealth();
|
|
266
301
|
clearInterval(this.terminalMailTimer); await this.terminalMailWork;
|
|
267
302
|
clearInterval(this.inputRepairTimer);this.inputRepairTimer=null;
|
|
268
303
|
await this.inputRepairWork;
|
|
@@ -520,7 +555,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
520
555
|
}
|
|
521
556
|
|
|
522
557
|
#noticeTarget(address){
|
|
523
|
-
return sessionNoticeTarget(address,{machine:
|
|
558
|
+
return sessionNoticeTarget(address,{machine:machineName(this.env),
|
|
524
559
|
mail:request=>this.#nativeMailReply(request),
|
|
525
560
|
discover:params=>this.#serveRequest('harness.v1.sessions.discover',params,10000),
|
|
526
561
|
recordedConfig:locator=>this.#serveRequest('harness.v1.sessions.recorded_config',{locator},15000)});
|
|
@@ -1528,6 +1563,34 @@ export class TeamsMachine extends MachineCore {
|
|
|
1528
1563
|
if (!this.machineEvents || params.team_id!==this.machineEvents.teamId) throw new Error('this connector is not serving that team');
|
|
1529
1564
|
return method==='machine.events.events'?this.machineEvents.snapshot():this.machineEvents.subscribe(params.address,method.endsWith('.unsubscribe'));
|
|
1530
1565
|
}
|
|
1566
|
+
case 'health.read': {
|
|
1567
|
+
// A machine's vitals: operators, and principals granted view on the whole machine.
|
|
1568
|
+
if (!operator) need('view', '*');
|
|
1569
|
+
const dir = healthDirOf(this.env);
|
|
1570
|
+
let snapshot = null;
|
|
1571
|
+
try { snapshot = JSON.parse(readFileSync(join(dir, 'latest.json'), 'utf8')); } catch { /* no reading yet */ }
|
|
1572
|
+
let subscriptions = [];
|
|
1573
|
+
try { subscriptions = JSON.parse(readFileSync(join(dir, 'subscriptions.json'), 'utf8')).addresses ?? []; } catch { /* none */ }
|
|
1574
|
+
return { snapshot, probe: this.healthRun ?? { running: false, reason: 'not started by this daemon' }, subscriptions, dir };
|
|
1575
|
+
}
|
|
1576
|
+
case 'health.subscribe':
|
|
1577
|
+
case 'health.unsubscribe': {
|
|
1578
|
+
// Alarm mail for a session or board address. Operators only: it decides who is woken.
|
|
1579
|
+
if (!operator) throw Object.assign(new Error('subscribing to machine health requires operator authority'), { code: 403 });
|
|
1580
|
+
const address = String(params.address ?? '');
|
|
1581
|
+
if (!/^sc:[^:\s]+:[^:\s]+:\S+$/.test(address)) throw new Error('health.subscribe needs a full sc:<machine>:<harness>:<id> address');
|
|
1582
|
+
const dir = healthDirOf(this.env);
|
|
1583
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1584
|
+
const file = join(dir, 'subscriptions.json');
|
|
1585
|
+
let addresses = [];
|
|
1586
|
+
try { addresses = JSON.parse(readFileSync(file, 'utf8')).addresses ?? []; } catch { /* first */ }
|
|
1587
|
+
addresses = addresses.filter((a) => a !== address);
|
|
1588
|
+
if (method === 'health.subscribe') addresses.push(address);
|
|
1589
|
+
writeFileSync(`${file}.${process.pid}.tmp`, JSON.stringify({ addresses }) + '\n', { mode: 0o600 });
|
|
1590
|
+
renameSync(`${file}.${process.pid}.tmp`, file);
|
|
1591
|
+
act(method, address);
|
|
1592
|
+
return { addresses };
|
|
1593
|
+
}
|
|
1531
1594
|
case 'harness.v1.agents.bindSession': {
|
|
1532
1595
|
if (!operator) throw Object.assign(new Error('binding an agent session requires operator authority'),{code:403});
|
|
1533
1596
|
const {session,context}=params;
|
|
@@ -1575,7 +1638,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
1575
1638
|
const ended=await this.#serveRequest('harness.v1.runtimes.close',{connection:runtime.connection});
|
|
1576
1639
|
result=ended?.closed?{state:'closed',reason:'native runtime close receipt'}:{state:'unknown',reason:'runtime returned no close receipt'};
|
|
1577
1640
|
}else{
|
|
1578
|
-
const machine=
|
|
1641
|
+
const machine=machineName(this.env),address=`sc:${machine}:${closure.harness}:${closure.native_id}`;
|
|
1579
1642
|
const code=await new Promise((resolve,reject)=>{
|
|
1580
1643
|
const child=spawn(this.supercodeBin,['close',address],{env:{...this.env,SUPERCODE_CALLER:address},stdio:'ignore'});
|
|
1581
1644
|
const timer=setTimeout(()=>{child.kill('SIGTERM');resolve(null);},30000);
|
|
@@ -2293,3 +2356,17 @@ function alive(pid) {
|
|
|
2293
2356
|
}
|
|
2294
2357
|
|
|
2295
2358
|
export { listAnnouncedPorts };
|
|
2359
|
+
|
|
2360
|
+
/** The machine-health pack's state directory: the pack's own rule (sdk/health/lib/paths.mjs). */
|
|
2361
|
+
function healthDirOf(env) {
|
|
2362
|
+
if (env.SUPERCODE_HEALTH_DIR) return env.SUPERCODE_HEALTH_DIR;
|
|
2363
|
+
return join(env.SUPERCODE_HOME || join(env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'supercode'), 'health');
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
/** The pack's probe entry: an explicit path, the installed package, else a source checkout's sibling. */
|
|
2367
|
+
function healthEntry(env) {
|
|
2368
|
+
if (env.SUPERCODE_HEALTH_ENTRY) return existsSync(env.SUPERCODE_HEALTH_ENTRY) ? env.SUPERCODE_HEALTH_ENTRY : null;
|
|
2369
|
+
try { const found = fileURLToPath(import.meta.resolve('@volter/supercode-health/bin')); if (existsSync(found)) return found; } catch { /* not installed */ }
|
|
2370
|
+
const sibling = fileURLToPath(new URL('../health/bin/health.mjs', import.meta.url));
|
|
2371
|
+
return existsSync(sibling) ? sibling : null;
|
|
2372
|
+
}
|
package/mail-history.mjs
CHANGED
|
@@ -71,7 +71,7 @@ async function answeredMail(rows, binary, env, signal) {
|
|
|
71
71
|
|
|
72
72
|
// Match the CLI's local_machine_name: the native mailbox belongs to its
|
|
73
73
|
// enrolled context, which can differ from a viewer connector's --home.
|
|
74
|
-
function machineName(env) {
|
|
74
|
+
export function machineName(env) {
|
|
75
75
|
let name = hostname();
|
|
76
76
|
try {
|
|
77
77
|
const root = join(teamsHome(env), 'workspaces');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.14.1",
|
|
62
62
|
"@volter/supercode-frontend": "^0.2.2",
|
|
63
63
|
"@volter/supercode-harness-sdk": "^0.3.43",
|
|
64
|
+
"@volter/supercode-health": "^0.1.0",
|
|
64
65
|
"@volter/supercode-terminal": "^0.2.54",
|
|
65
66
|
"@volter/identity": "^0.4.1",
|
|
66
67
|
"@volter/teams": "0.2.1",
|