@volter/supercode-teams 0.2.111 → 0.2.112

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/machine.mjs CHANGED
@@ -48,6 +48,7 @@ import { request as httpRequest } from 'node:http';
48
48
  import { DEFAULT_RANK, normalizeRank, judge } from './attention.mjs';
49
49
  import { popupTerminal, openPopup } from './popup-terminals.mjs';
50
50
  import { JobOwner, executionCapabilities, streamProgram, serveWorkspace } from './execution.mjs';
51
+ import { confirmNativeInitialInput } from './native-initial-input.mjs';
51
52
 
52
53
  /** The preload every launched pane carries: it announces the ports a pane's node processes listen on. */
53
54
  const PORTHOLE_HOOK = fileURLToPath(new URL('./porthole-hook.cjs', import.meta.url));
@@ -577,6 +578,52 @@ export class TeamsMachine extends MachineCore {
577
578
  return env;
578
579
  }
579
580
 
581
+ // Native session hooks carry the launcher's identity across Codex's shared
582
+ // app-server process boundary. Process ancestry alone cannot recover it.
583
+ #nativeQuestionCommand(params, pane, by, nativeInput) {
584
+ const args = params.arguments ?? [];
585
+ const name = String(params.program ?? '').split(/[\\/]/).pop();
586
+ if (process.platform === 'win32' || !((params.kind === 'codex' && name === 'codex') ||
587
+ (params.kind === 'claude-code' && name === 'claude'))) return launchCommand(params.program, args);
588
+ this.nativeQuestionHooksSupported ??= spawnSync(this.supercodeBin, ['message', 'native-question-hook', '--help'], { env: this.env, timeout: 2000, stdio: 'ignore' }).status === 0;
589
+ if (!this.nativeQuestionHooksSupported) return launchCommand(params.program, args);
590
+ const mailHome = this.env.SUPERCODE_HOME || join(this.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'supercode');
591
+ const dir = join(mailHome, 'mail', 'native-launches');
592
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
593
+ // A reused pane key still gets a new receipt baseline. A late hook from
594
+ // its previous process cannot publish evidence for this launch.
595
+ const binding = join(dir, `${pane}-${randomBytes(9).toString('base64url')}.json`);
596
+ const initialInput = params.initialInput && !args.includes('--') &&
597
+ !args.some((arg) => arg === '--remote' || arg.startsWith('--remote=')) ? params.initialInput : null;
598
+ writeFileSync(binding, JSON.stringify({ kind: params.kind, pane, by, program: params.program, arguments: args, initialInput }), { mode: 0o600 });
599
+ if (initialInput) nativeInput({ binding, kind: params.kind, text: initialInput, startedAt: Date.now() });
600
+ const claudeCommand = (next) => launchCommand(params.program, initialInput ? [...next, '--', initialInput] : next);
601
+ const quote = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
602
+ const command = `env SUPERCODE_HOME=${quote(mailHome)} ${quote(this.supercodeBin)} message native-question-hook ${quote(binding)}`;
603
+ const handler = { type: 'command', command, timeout: 3600 };
604
+ if (params.kind === 'claude-code') {
605
+ const hooks = { SessionStart: [{ hooks: [handler] }], PreToolUse: [{ matcher: 'AskUserQuestion', hooks: [handler] }], PostToolUse: [{ matcher: 'AskUserQuestion', hooks: [handler] }] };
606
+ // Claude accepts only one settings value. Preserve an existing inline
607
+ // object or file, including its hook arrays, in that same argument.
608
+ const next = [...args];
609
+ const at = next.findIndex((arg) => arg === '--settings' || arg.startsWith('--settings='));
610
+ if (at < 0) return claudeCommand([...next, '--settings', JSON.stringify({ hooks })]);
611
+ const inline = next[at].startsWith('--settings=');
612
+ const value = inline ? next[at].slice('--settings='.length) : next[at + 1];
613
+ const settings = JSON.parse(value?.trimStart().startsWith('{') ? value : readFileSync(resolve(params.cwd || process.cwd(), value), 'utf8'));
614
+ if (!settings || Array.isArray(settings) || typeof settings !== 'object') throw new Error('Claude --settings must be an object to add native question mail');
615
+ settings.hooks ??= {};
616
+ for (const [event, groups] of Object.entries(hooks)) settings.hooks[event] = [...(settings.hooks[event] ?? []), ...groups];
617
+ next.splice(at, inline ? 1 : 2, '--settings', JSON.stringify(settings));
618
+ return claudeCommand(next);
619
+ }
620
+ // The stock TUI's default embedded server has no attachable question
621
+ // door. Give this pane its own native server, through Codex's --remote
622
+ // Unix endpoint; never attach to another running session by inference.
623
+ if (args.some((arg) => arg === '--remote' || arg.startsWith('--remote='))) return launchCommand(params.program, args);
624
+ return launchCommand(process.execPath, [fileURLToPath(new URL('./native-codex-pane.mjs', import.meta.url)), binding]);
625
+ }
626
+
580
627
  /**
581
628
  * A session acts only on the panes it started: input, a turn, a suspend or a close from a session (`caller`, its
582
629
  * address) on a pane another session, a board or a person started is refused, naming who started it. A person (no
@@ -747,9 +794,18 @@ export class TeamsMachine extends MachineCore {
747
794
  * Never rejects. The only caller is a fire-and-forget from the request path,
748
795
  * where an unhandled rejection would take the daemon down over one pane.
749
796
  */
750
- #deliverInitialInput({ key, contextKey, session, kind, by, input, act }) {
797
+ #deliverInitialInput({ key, contextKey, session, kind, by, input, act, nativeInput }) {
751
798
  const abort = new AbortController();
752
799
  const task = (async () => {
800
+ if (nativeInput) {
801
+ const result = await confirmNativeInitialInput(nativeInput, { signal: abort.signal,
802
+ deadlineMs: Date.now() + INITIAL_INPUT_READY_DEADLINE_MS, live: async () => Boolean(await this.#paneByContext(contextKey)) });
803
+ this.launches.record({ key, pane: contextKey, kind, by, status: 'ready', inputDelivered: result.delivered,
804
+ inputReadyBy: 'native-prompt', ...(result.delivered ? { inputReceipt: result.receipt } : { inputError: result.reason }) });
805
+ act('launch', `pane:${contextKey}`, result.delivered ? 'ok' : 'failed',
806
+ result.delivered ? 'first input confirmed by native user message' : `native first input unconfirmed: ${result.reason}`);
807
+ return;
808
+ }
753
809
  const readyBy = await this.#awaitInputReady(session, contextKey, kind, abort.signal);
754
810
  if (readyBy === 'aborted') return;
755
811
  if (readyBy === 'pane-gone' || readyBy === 'deadline') {
@@ -1229,9 +1285,10 @@ export class TeamsMachine extends MachineCore {
1229
1285
  this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, status: 'issued' });
1230
1286
  if (params.kind === 'claude-code' && params.cwd && (params.arguments ?? []).includes('--dangerously-skip-permissions')) trustClaudeFolder(params.cwd);
1231
1287
  let session;
1288
+ let nativeInput;
1232
1289
  try {
1233
1290
  session = await this.host.createSession({
1234
- command: launchCommand(params.program, params.arguments ?? []),
1291
+ command: this.#nativeQuestionCommand(params, contextKey, by, (value) => { nativeInput = value; }),
1235
1292
  contextKey,
1236
1293
  cwd: params.cwd,
1237
1294
  // A pane that runs one verb for its launcher (`close`/`stop` on another machine) acts as that launcher.
@@ -1261,7 +1318,7 @@ export class TeamsMachine extends MachineCore {
1261
1318
  // outcome is recorded against the launch key, so `launches.get|list`
1262
1319
  // answers "did my message go in, and was it late?" without anyone
1263
1320
  // reading the screen.
1264
- if (params.initialInput) this.#deliverInitialInput({ key, contextKey, session, kind: params.kind ?? null, by, input: params.initialInput, act });
1321
+ if (params.initialInput) this.#deliverInitialInput({ key, contextKey, session, kind: params.kind ?? null, by, input: params.initialInput, act, nativeInput });
1265
1322
  // A program that ends at once (not found, not runnable, failing on its first line) takes its pane with it:
1266
1323
  // the caller is told, with its exit status, instead of being handed a pane that is already gone.
1267
1324
  await new Promise((resolve) => setTimeout(resolve, LAUNCH_SETTLE_MS));
@@ -0,0 +1,54 @@
1
+ // A native Codex TUI and its native app-server share one private local endpoint.
2
+ // Supercode hosts neither the agent nor its run loop; it can attach to native
3
+ // question requests while the stock TUI remains the session's UI.
4
+ import { spawn } from 'node:child_process';
5
+ import { readFileSync, writeFileSync, renameSync, mkdtempSync, existsSync, rmSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+
9
+ const path = process.argv[2];
10
+ const seed = JSON.parse(readFileSync(path, 'utf8'));
11
+ const directory = mkdtempSync(join(tmpdir(), 'sc-codex-'));
12
+ const socketPath = join(directory, 'native.sock');
13
+ const endpoint = `unix://${socketPath}`;
14
+ // Configuration is evaluated by the native server. Model, approval and
15
+ // workspace options remain the TUI's own thread-start/resume arguments.
16
+ const config = [];
17
+ for (let index = 0; index < seed.arguments.length; index += 1) {
18
+ const arg = seed.arguments[index];
19
+ if (['-c', '--config', '--enable', '--disable'].includes(arg)) config.push(arg, seed.arguments[++index]);
20
+ else if (/^--(?:config|enable|disable)=/.test(arg)) config.push(arg);
21
+ }
22
+ const server = spawn(seed.program, ['app-server', '--listen', endpoint, ...config], { stdio: ['ignore', 'ignore', 'inherit'] });
23
+ let tui;
24
+ let ended = false;
25
+ let serverError;
26
+ server.on('error', (error) => { serverError = error; });
27
+ server.on('exit', () => { ended = true; tui?.kill('SIGTERM'); });
28
+ const stop = () => { tui?.kill('SIGTERM'); server.kill('SIGTERM'); };
29
+ for (const signal of ['SIGTERM', 'SIGHUP', 'SIGINT']) process.on(signal, stop);
30
+ // A killed tmux pane can orphan the wrapper without delivering a signal.
31
+ // Its original parent disappearing closes only these two owned processes.
32
+ const parent = process.ppid;
33
+ const guard = setInterval(() => { if (process.ppid !== parent) stop(); }, 1000);
34
+ try {
35
+ const deadline = Date.now() + 15_000;
36
+ while (!existsSync(socketPath) && !ended && !serverError && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 50));
37
+ if (!existsSync(socketPath) || serverError || ended) throw serverError ?? new Error('Native Codex question endpoint did not start; no TUI input was sent.');
38
+ const staged = `${path}.${process.pid}`;
39
+ writeFileSync(staged, JSON.stringify({ ...seed, socketPath, ownerPid: process.pid }), { mode: 0o600 });
40
+ renameSync(staged, path);
41
+ const args = [...seed.arguments, '--remote', endpoint];
42
+ if (!args.some((arg) => arg === '-C' || arg === '--cd' || arg.startsWith('--cd='))) args.push('--cd', process.cwd());
43
+ if (seed.initialInput) args.push('--', seed.initialInput);
44
+ tui = spawn(seed.program, args, { stdio: 'inherit' });
45
+ process.exitCode = await new Promise((resolve, reject) => { tui.once('exit', (code) => resolve(code ?? 1)); tui.once('error', reject); });
46
+ } catch (error) {
47
+ process.stderr.write(`supercode: ${error.message}\n`);
48
+ process.exitCode = 1;
49
+ } finally {
50
+ clearInterval(guard);
51
+ stop();
52
+ if (!ended) await Promise.race([new Promise((resolve) => server.once('exit', resolve)), new Promise((resolve) => setTimeout(() => { server.kill('SIGKILL'); resolve(); }, 3000))]);
53
+ rmSync(directory, { recursive: true, force: true });
54
+ }
@@ -0,0 +1,64 @@
1
+ // Native CLIs accept an initial prompt before their TUI starts. Their own
2
+ // transcript, not a collapsed paste badge or an empty editor, confirms receipt.
3
+ import { open, readFile } from 'node:fs/promises';
4
+ import { StringDecoder } from 'node:string_decoder';
5
+ import { codexQuestions } from './native-questions.mjs';
6
+
7
+ const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8
+
9
+ export async function confirmNativeInitialInput({ binding, kind, text, startedAt }, { signal, deadlineMs, live }) {
10
+ const readers = new Map();
11
+ const expired = () => signal.aborted || Date.now() >= deadlineMs;
12
+ try {
13
+ while (!expired()) {
14
+ if (!await live()) return { delivered: false, reason: 'pane-gone' };
15
+ let paths = [];
16
+ try {
17
+ if (kind === 'claude-code') {
18
+ paths = [JSON.parse(await readFile(binding.replace(/\.json$/, '.session.json'), 'utf8'))];
19
+ } else {
20
+ const seed = JSON.parse(await readFile(binding, 'utf8'));
21
+ if (seed.socketPath) {
22
+ const snapshot = await codexQuestions({ socketPath: seed.socketPath, discover: true, pathsOnly: true,
23
+ timeoutMs: Math.min(2000, Math.max(1, deadlineMs - Date.now())) });
24
+ paths = snapshot.threads.map(({ path }) => ({ path, offset: 0 }));
25
+ }
26
+ }
27
+ } catch { /* Startup may not yet have published its native session. */ }
28
+ for (const entry of paths) {
29
+ if (!entry.path) continue;
30
+ let reader = readers.get(entry.path);
31
+ if (!reader) {
32
+ try {
33
+ reader = { file: await open(entry.path, 'r'), position: entry.offset, decoder: new StringDecoder('utf8'), buffered: '' };
34
+ readers.set(entry.path, reader);
35
+ } catch { continue; }
36
+ }
37
+ const bytes = Buffer.alloc(64 * 1024);
38
+ while (!expired()) {
39
+ const { bytesRead } = await reader.file.read(bytes, 0, bytes.length, reader.position);
40
+ if (!bytesRead) break;
41
+ reader.position += bytesRead;
42
+ reader.buffered += reader.decoder.write(bytes.subarray(0, bytesRead));
43
+ let newline;
44
+ while ((newline = reader.buffered.indexOf('\n')) >= 0) {
45
+ const line = reader.buffered.slice(0, newline); reader.buffered = reader.buffered.slice(newline + 1);
46
+ let record;
47
+ try { record = JSON.parse(line); } catch { continue; }
48
+ let content;
49
+ if (kind === 'claude-code' && record.type === 'user' && !record.isMeta) content = record.message?.content;
50
+ if (kind === 'codex' && record.type === 'response_item' && record.payload?.type === 'message' &&
51
+ record.payload.role === 'user' && Date.parse(record.timestamp) >= startedAt) content = record.payload.content;
52
+ const value = typeof content === 'string' ? content : Array.isArray(content) &&
53
+ content.every((part) => ['text', 'input_text'].includes(part.type)) ? content.map((part) => part.text).join('') : null;
54
+ if (value === text) return { delivered: true, receipt: 'native-user-message' };
55
+ }
56
+ }
57
+ }
58
+ await pause(250);
59
+ }
60
+ return { delivered: false, reason: signal.aborted ? 'aborted' : 'native-input-unconfirmed' };
61
+ } finally {
62
+ await Promise.all([...readers.values()].map(({ file }) => file.close()));
63
+ }
64
+ }
@@ -0,0 +1,160 @@
1
+ // Native Codex TUI questions, through the same local app-server that owns the
2
+ // TUI's running thread. This never starts a runtime or writes terminal input.
3
+ import { connect } from 'node:net';
4
+ import { open } from 'node:fs/promises';
5
+ import { StringDecoder } from 'node:string_decoder';
6
+ import { pathToFileURL } from 'node:url';
7
+ import WebSocket from 'ws';
8
+
9
+ const questionMethod = 'item/tool/requestUserInput';
10
+ const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+ const failure = (code, message) => Object.assign(new Error(message), { code });
12
+
13
+ function identity(params) {
14
+ return { threadId: params.threadId, turnId: params.turnId, itemId: params.itemId };
15
+ }
16
+
17
+ function sameQuestion(left, right) {
18
+ return ['threadId', 'turnId', 'itemId'].every((key) => typeof left?.[key] === 'string' && left[key] === right?.[key]);
19
+ }
20
+
21
+ /** Read or answer one running native TUI thread. A response's id is scoped to
22
+ * its connection, so reattach and match the stable thread/turn/item identity;
23
+ * never persist the JSON-RPC id as the identity of a question.
24
+ */
25
+ export async function codexQuestions({ threadId, threadIds, answer, socketPath, discover = false, pathsOnly = false, timeoutMs = 15_000 } = {}) {
26
+ const targets = new Set(threadIds ?? (threadId ? [threadId] : []));
27
+ if ((!targets.size && !(discover && socketPath)) || [...targets].some((id) => typeof id !== 'string' || !id) || (answer && targets.size !== 1)) throw failure('invalid-request', 'Native thread ids are required; answer only one thread at a time.');
28
+ if (!socketPath) throw failure('native-question-unavailable', 'This launch has no native question endpoint.');
29
+ const socket = new WebSocket('ws://localhost/', { perMessageDeflate: false, createConnection: () => connect(socketPath), handshakeTimeout: timeoutMs });
30
+ let nextId = 0;
31
+ const pending = new Map();
32
+ const questions = new Map();
33
+ const threads = [];
34
+ let submitted = null;
35
+ let resolved = false;
36
+ let transcript;
37
+ let transcriptPath;
38
+ let ended = null;
39
+ const request = (method, params) => new Promise((resolve, reject) => {
40
+ const id = ++nextId;
41
+ pending.set(id, { resolve, reject });
42
+ socket.send(JSON.stringify({ id, method, params }));
43
+ });
44
+ const failPending = (error) => {
45
+ ended = error;
46
+ for (const waiter of pending.values()) waiter.reject(error);
47
+ pending.clear();
48
+ };
49
+ socket.on('error', failPending);
50
+ socket.on('close', () => failPending(failure('native-question-disconnected', 'The native question connection closed.')));
51
+ socket.on('message', (bytes) => {
52
+ let message;
53
+ try { message = JSON.parse(bytes); } catch { return; }
54
+ if (!message.method) {
55
+ const waiter = pending.get(message.id);
56
+ if (waiter) {
57
+ pending.delete(message.id);
58
+ if (message.error) waiter.reject(failure('native-question-refused', message.error.message));
59
+ else waiter.resolve(message.result);
60
+ }
61
+ return;
62
+ }
63
+ if (!targets.has(message.params?.threadId)) return;
64
+ if (message.method === questionMethod) questions.set(message.id, message.params);
65
+ if (message.method === 'serverRequest/resolved') {
66
+ questions.delete(message.params.requestId);
67
+ if (submitted === message.params.requestId) resolved = true;
68
+ }
69
+ });
70
+ const timer = setTimeout(() => {
71
+ failPending(failure('native-question-timeout', submitted === null ? 'The native question did not answer in time.' : 'The answer is unconfirmed; do not resend it automatically.'));
72
+ socket.terminate();
73
+ }, timeoutMs);
74
+ try {
75
+ await new Promise((resolve, reject) => { socket.once('open', resolve); socket.once('error', reject); });
76
+ await request('initialize', { clientInfo: { name: 'supercode_native_questions', version: '1' }, capabilities: { experimentalApi: true } });
77
+ socket.send(JSON.stringify({ method: 'initialized' }));
78
+ // Resuming a persisted thread would start another native execution. Only
79
+ // attach when the daemon itself lists this thread as already loaded.
80
+ let cursor;
81
+ const loaded = new Set();
82
+ do {
83
+ const page = await request('thread/loaded/list', { ...(cursor ? { cursor } : {}), limit: 100 });
84
+ for (const id of page.data ?? []) if (discover || targets.has(id)) loaded.add(id);
85
+ cursor = page.nextCursor;
86
+ } while (cursor && (discover || loaded.size < targets.size));
87
+ if (answer && !loaded.has(threadId)) throw failure('native-thread-not-running', 'The native daemon does not have this thread loaded; nothing was started.');
88
+ for (const id of loaded) {
89
+ const snapshot = await request('thread/read', { threadId: id, includeTurns: false });
90
+ if (snapshot.thread?.ephemeral) continue;
91
+ if (answer) transcriptPath = snapshot.thread?.path;
92
+ targets.add(id);
93
+ threads.push({ id, path: snapshot.thread?.path });
94
+ if (pathsOnly) continue;
95
+ if (!answer && snapshot.thread?.status?.type === 'idle') continue;
96
+ await request('thread/resume', { threadId: id, excludeTurns: true });
97
+ }
98
+ // The native server replays unresolved requests after its resume response.
99
+ if (!pathsOnly) await pause(200);
100
+ if (ended) throw ended;
101
+ if (!answer) return { threads, questions: [...questions.values()].map((question) => ({ ...identity(question), questions: question.questions })), transport: 'codex-native-app-server' };
102
+ const current = [...questions].find(([, question]) => sameQuestion(question, answer.question));
103
+ if (!current) return { delivered: false, reason: 'question-no-longer-pending' };
104
+ const [id, question] = current;
105
+ const wanted = question.questions.map((entry) => entry.id);
106
+ if (!answer.answers || Object.keys(answer.answers).length !== wanted.length || wanted.some((key) => !Array.isArray(answer.answers[key]?.answers) || answer.answers[key].answers.length === 0 || answer.answers[key].answers.some((text) => typeof text !== 'string'))) {
107
+ throw failure('invalid-answer', 'Answer every native question id with a nonempty answers array.');
108
+ }
109
+ // Synchronous from the last pending-state read through send: no sleep,
110
+ // subprocess or terminal operation can insert an answer to another prompt.
111
+ if (!transcriptPath) return { delivered: false, reason: 'native-receipt-unavailable' };
112
+ transcript = await open(transcriptPath, 'r');
113
+ let position = (await transcript.stat()).size;
114
+ let buffered = '';
115
+ const decoder = new StringDecoder('utf8');
116
+ const bytes = Buffer.alloc(64 * 1024);
117
+ submitted = id;
118
+ socket.send(JSON.stringify({ id, result: { answers: answer.answers } }));
119
+ // A resolved request alone could have been answered by the TUI racing us.
120
+ // Confirm the native tool result carries these answers for this call id.
121
+ let matched = false;
122
+ while (!ended) {
123
+ const { bytesRead } = await transcript.read(bytes, 0, bytes.length, position);
124
+ position += bytesRead;
125
+ buffered += decoder.write(bytes.subarray(0, bytesRead));
126
+ let newline;
127
+ while ((newline = buffered.indexOf('\n')) >= 0) {
128
+ const line = buffered.slice(0, newline); buffered = buffered.slice(newline + 1);
129
+ let item;
130
+ try { item = JSON.parse(line).payload; } catch { continue; }
131
+ if (item?.type !== 'function_call_output' || item.call_id !== answer.question.itemId) continue;
132
+ let result;
133
+ try { result = typeof item.output === 'string' ? JSON.parse(item.output) : item.output; } catch { continue; }
134
+ matched = wanted.every((key) => JSON.stringify(result?.answers?.[key]?.answers) === JSON.stringify(answer.answers[key].answers));
135
+ if (!matched) return { delivered: false, reason: 'native-question-answered-differently' };
136
+ }
137
+ if (resolved && matched) return { delivered: true, question: identity(answer.question), transport: 'codex-native-app-server', receipt: 'native-tool-result' };
138
+ await pause(25);
139
+ }
140
+ throw ended;
141
+ } finally {
142
+ clearTimeout(timer);
143
+ await transcript?.close();
144
+ socket.close();
145
+ }
146
+ }
147
+
148
+ // The Rust mailbox calls this narrow adapter. stdin carries JSON so answer
149
+ // text never becomes shell syntax or a process-list argument.
150
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
151
+ try {
152
+ const chunks = [];
153
+ for await (const chunk of process.stdin) chunks.push(chunk);
154
+ const result = await codexQuestions(JSON.parse(Buffer.concat(chunks).toString('utf8')));
155
+ process.stdout.write(`${JSON.stringify(result)}\n`);
156
+ } catch (error) {
157
+ process.stdout.write(`${JSON.stringify({ delivered: false, reason: error.code ?? 'native-question-error', detail: error.message })}\n`);
158
+ process.exitCode = 1;
159
+ }
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.111",
3
+ "version": "0.2.112",
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",