@nonbot/cli 0.9.1 → 0.9.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.
@@ -4,6 +4,7 @@ import * as activations from '../lib/activations.js';
4
4
  import { fireActivation, makeHeadlessSpawner, } from '../lib/activations.js';
5
5
  import { checkCompletions } from '../lib/completion.js';
6
6
  import { deliverAnsweredPrompts } from '../lib/run-prompt.js';
7
+ import { loadOrCreateMachineId, resolveMachineName } from '../lib/machine.js';
7
8
  import { emitRunStage as defaultEmitRunStage, startRunHeartbeat as defaultStartRunHeartbeat, RUN_STAGE, } from '../lib/choir/run-progress.js';
8
9
  import { groupBySession, launchCoordinatedSet, } from '../lib/choir/coordinated-set.js';
9
10
  import { applyPaneTitle } from '../lib/pane-title.js';
@@ -177,6 +178,8 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
177
178
  }
178
179
  const detectTmux = deps.detectTmuxSession ?? detectTmuxSession;
179
180
  const tmuxSessionName = detectTmux();
181
+ const machineId = deps.machineId ?? loadOrCreateMachineId();
182
+ const machineName = resolveMachineName();
180
183
  if (tmuxSessionName) {
181
184
  applyNonbotTmuxConfig({ spawnSync: deps.spawnSync });
182
185
  }
@@ -304,6 +307,8 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
304
307
  'X-CLI-Version': VERSION,
305
308
  'X-Terminal-Kind': terminalKind,
306
309
  'X-Terminal-Launchable': terminalLaunchable ? 'true' : 'false',
310
+ 'X-Machine-Id': machineId,
311
+ 'X-Machine-Name': machineName,
307
312
  };
308
313
  if (tmuxSessionName)
309
314
  headers['X-Tmux-Session'] = tmuxSessionName;
@@ -460,19 +465,18 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
460
465
  emitSummary();
461
466
  }
462
467
  }
463
- if (trackedPanes.size > 0) {
464
- try {
465
- await deliverAnsweredPrompts({
466
- baseUrl: auth.baseUrl,
467
- pat: auth.pat,
468
- injected: injectedPrompts,
469
- fetchImpl,
470
- spawnImpl: deps.spawnSync,
471
- log,
472
- });
473
- }
474
- catch {
475
- }
468
+ try {
469
+ await deliverAnsweredPrompts({
470
+ baseUrl: auth.baseUrl,
471
+ pat: auth.pat,
472
+ injected: injectedPrompts,
473
+ machineId,
474
+ fetchImpl,
475
+ spawnImpl: deps.spawnSync,
476
+ log,
477
+ });
478
+ }
479
+ catch {
476
480
  }
477
481
  if (trackedPanes.size > 0) {
478
482
  const reported = await checkCompletions({
@@ -0,0 +1,51 @@
1
+ import { homedir, hostname } from 'node:os';
2
+ import { randomUUID } from 'node:crypto';
3
+ import * as nodeFs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ const NAME_MAX = 64;
6
+ function configDir() {
7
+ const override = process.env.NONBOT_CONFIG_DIR;
8
+ if (override && override.length > 0)
9
+ return override;
10
+ return path.join(homedir(), '.config', 'nonbot');
11
+ }
12
+ export function machineFilePath(dir = configDir()) {
13
+ return path.join(dir, 'machine.json');
14
+ }
15
+ export function loadOrCreateMachineId(deps = {}) {
16
+ const fs = deps.fs ?? nodeFs;
17
+ const uuid = deps.uuid ?? randomUUID;
18
+ const dir = deps.dir ?? configDir();
19
+ const file = path.join(dir, 'machine.json');
20
+ try {
21
+ if (fs.existsSync(file)) {
22
+ const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
23
+ if (raw && typeof raw.machineId === 'string' && raw.machineId.length > 0) {
24
+ return raw.machineId;
25
+ }
26
+ }
27
+ }
28
+ catch {
29
+ }
30
+ const machineId = uuid();
31
+ try {
32
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
33
+ fs.writeFileSync(file, JSON.stringify({ machineId, createdAt: Date.now() }), { mode: 0o600 });
34
+ }
35
+ catch {
36
+ }
37
+ return machineId;
38
+ }
39
+ export function resolveMachineName(env = process.env) {
40
+ const override = env.NONBOT_MACHINE_NAME;
41
+ if (override && override.trim().length > 0)
42
+ return override.trim().slice(0, NAME_MAX);
43
+ try {
44
+ const h = hostname();
45
+ if (h && h.length > 0)
46
+ return h.slice(0, NAME_MAX);
47
+ }
48
+ catch {
49
+ }
50
+ return 'unknown';
51
+ }
@@ -4,6 +4,9 @@ import { VERSION } from '../version.js';
4
4
  const QUESTION_MAX = 200;
5
5
  const LABEL_MAX = 80;
6
6
  const OPTIONS_MAX = 8;
7
+ const PANE_ID_RE = /^%\d+$/;
8
+ const ANSWERED_MAX = 16;
9
+ const ANSWER_TEXT_MAX = 4096;
7
10
  const MENU_LINE_RE = /^\s*[>❯▸▶*•]?\s*(\d{1,2})[.)]\s+(\S.*)$/;
8
11
  const GLYPH_RE = /[│┃|╭╮╯╰─━┄┈┌┐└┘├┤>❯▸▶*•]/g;
9
12
  export function parsePromptMenu(paneText, fallbackMessage = '') {
@@ -96,20 +99,21 @@ export async function reportPrompt(args) {
96
99
  export async function pollAnsweredPrompts(args) {
97
100
  const fetchImpl = args.fetchImpl ?? fetch;
98
101
  try {
99
- const res = await fetchImpl(`${args.baseUrl}/api/cli/run-prompts/answered`, {
100
- headers: {
101
- Authorization: `Bearer ${args.pat}`,
102
- 'X-Requested-With': 'ConradPM-Native',
103
- 'X-CLI-Version': VERSION,
104
- },
105
- });
102
+ const headers = {
103
+ Authorization: `Bearer ${args.pat}`,
104
+ 'X-Requested-With': 'ConradPM-Native',
105
+ 'X-CLI-Version': VERSION,
106
+ };
107
+ if (args.machineId)
108
+ headers['X-Machine-Id'] = args.machineId;
109
+ const res = await fetchImpl(`${args.baseUrl}/api/cli/run-prompts/answered`, { headers });
106
110
  if (!res.ok)
107
111
  return [];
108
112
  const data = (await res.json());
109
113
  if (!data || !Array.isArray(data.prompts))
110
114
  return [];
111
115
  const out = [];
112
- for (const r of data.prompts) {
116
+ for (const r of data.prompts.slice(0, ANSWERED_MAX)) {
113
117
  if (!r || typeof r.promptId !== 'string' || typeof r.activationId !== 'string')
114
118
  continue;
115
119
  out.push({
@@ -117,7 +121,7 @@ export async function pollAnsweredPrompts(args) {
117
121
  activationId: r.activationId,
118
122
  paneId: typeof r.paneId === 'string' && r.paneId.length > 0 ? r.paneId : null,
119
123
  answerIndex: typeof r.answerIndex === 'number' ? r.answerIndex : null,
120
- answerText: typeof r.answerText === 'string' ? r.answerText : null,
124
+ answerText: typeof r.answerText === 'string' ? r.answerText.slice(0, ANSWER_TEXT_MAX) : null,
121
125
  answeredAt: typeof r.answeredAt === 'number' ? r.answeredAt : undefined,
122
126
  });
123
127
  }
@@ -147,13 +151,15 @@ export async function confirmDelivered(args) {
147
151
  }
148
152
  }
149
153
  export function injectAnswer(paneId, answerIndex, answerText, spawnImpl = nodeSpawnSync) {
150
- const keys = answerIndex !== null && Number.isFinite(answerIndex)
154
+ if (!PANE_ID_RE.test(paneId))
155
+ return false;
156
+ const keys = answerIndex !== null && Number.isFinite(answerIndex) && answerIndex >= 0
151
157
  ? String(answerIndex)
152
158
  : answerText ?? '';
153
159
  if (keys.length === 0)
154
160
  return false;
155
161
  try {
156
- spawnImpl('tmux', ['send-keys', '-l', '-t', paneId, keys], {
162
+ spawnImpl('tmux', ['send-keys', '-l', '-t', paneId, '--', keys], {
157
163
  encoding: 'utf-8',
158
164
  timeout: 2000,
159
165
  windowsHide: true,
@@ -173,6 +179,7 @@ export async function deliverAnsweredPrompts(opts) {
173
179
  const answered = await pollAnsweredPrompts({
174
180
  baseUrl: opts.baseUrl,
175
181
  pat: opts.pat,
182
+ machineId: opts.machineId,
176
183
  fetchImpl: opts.fetchImpl,
177
184
  });
178
185
  if (answered.length === 0)
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.9.1';
1
+ export const VERSION = '0.9.3';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nonbot/cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "type": "module",
5
5
  "description": "The local host for non.bot ▶ Run — opens a terminal on your machine and starts the work in your linked repo.",
6
6
  "license": "UNLICENSED",