@nonbot/cli 0.9.12 → 0.10.0
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/CHANGELOG.md +247 -1
- package/dist/commands/choir.js +3 -0
- package/dist/commands/daemon.js +377 -9
- package/dist/commands/logs.js +6 -1
- package/dist/index.js +1 -1
- package/dist/lib/activations.js +46 -6
- package/dist/lib/activity-log.js +6 -0
- package/dist/lib/choir/hub.js +116 -18
- package/dist/lib/cloud-repo.js +306 -0
- package/dist/lib/command-builders.js +32 -1
- package/dist/lib/completion.js +125 -1
- package/dist/lib/daemon-lifecycle.js +44 -0
- package/dist/lib/exit-transcript.js +74 -0
- package/dist/lib/machine.js +7 -0
- package/dist/lib/payload-validator.js +51 -0
- package/dist/lib/run-prompt.js +38 -17
- package/dist/lib/snapshot.js +14 -12
- package/dist/lib/terminal.js +10 -4
- package/dist/lib/update-check.js +99 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/lib/completion.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
|
2
|
+
import * as nodeFs from 'node:fs';
|
|
3
|
+
import { exitFilePathFor } from './activations.js';
|
|
2
4
|
export function listLivePaneIds(spawnImpl = nodeSpawnSync) {
|
|
3
5
|
try {
|
|
4
6
|
const r = spawnImpl('tmux', ['list-panes', '-a', '-F', '#{pane_id}'], {
|
|
@@ -43,6 +45,55 @@ export async function postCompletion(baseUrl, pat, activationId, fetchImpl = fet
|
|
|
43
45
|
return false;
|
|
44
46
|
}
|
|
45
47
|
}
|
|
48
|
+
export function readExitCodeFile(activationId, fsImpl = nodeFs) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = fsImpl.readFileSync(exitFilePathFor(activationId), 'utf-8').trim();
|
|
51
|
+
if (raw.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
const n = Number(raw);
|
|
54
|
+
return Number.isInteger(n) && n >= 0 && n <= 255 ? n : null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function removeExitFile(activationId, fsImpl = nodeFs) {
|
|
61
|
+
try {
|
|
62
|
+
fsImpl.unlinkSync(exitFilePathFor(activationId));
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function postFailure(baseUrl, pat, activationId, failureReason, fetchImpl = fetch) {
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetchImpl(`${baseUrl}/api/portfolio/activations/${activationId}`, {
|
|
72
|
+
method: 'PATCH',
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${pat}`,
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
'X-Requested-With': 'ConradPM-Native',
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({ status: 'failed', failureReason: failureReason.slice(0, 1000) }),
|
|
79
|
+
});
|
|
80
|
+
return res.ok || res.status === 409;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export const MAX_REPORT_ATTEMPTS = 8;
|
|
87
|
+
export const TIMEOUT_EXIT_CODE = 124;
|
|
88
|
+
const reportAttemptsByTracker = new WeakMap();
|
|
89
|
+
function reportAttemptsFor(tracked) {
|
|
90
|
+
let m = reportAttemptsByTracker.get(tracked);
|
|
91
|
+
if (!m) {
|
|
92
|
+
m = new Map();
|
|
93
|
+
reportAttemptsByTracker.set(tracked, m);
|
|
94
|
+
}
|
|
95
|
+
return m;
|
|
96
|
+
}
|
|
46
97
|
export async function checkCompletions(opts) {
|
|
47
98
|
const { tracked, killed, baseUrl, pat } = opts;
|
|
48
99
|
if (tracked.size === 0)
|
|
@@ -51,14 +102,84 @@ export async function checkCompletions(opts) {
|
|
|
51
102
|
if (live === null)
|
|
52
103
|
return [];
|
|
53
104
|
const completed = detectCompletedActivations(tracked, live, killed);
|
|
105
|
+
const attempts = opts.cloud ? reportAttemptsFor(tracked) : null;
|
|
106
|
+
const noteFailedReport = (id) => {
|
|
107
|
+
if (!attempts)
|
|
108
|
+
return;
|
|
109
|
+
const n = (attempts.get(id) ?? 0) + 1;
|
|
110
|
+
if (n < MAX_REPORT_ATTEMPTS) {
|
|
111
|
+
attempts.set(id, n);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
attempts.delete(id);
|
|
115
|
+
tracked.delete(id);
|
|
116
|
+
opts.cloud?.forgetWorkspace?.(id);
|
|
117
|
+
opts.log?.(`⚠ ${id} · giving up after ${MAX_REPORT_ATTEMPTS} failed completion reports\n`);
|
|
118
|
+
};
|
|
54
119
|
for (const [id, pane] of [...tracked.entries()]) {
|
|
55
120
|
if (!live.has(pane) && killed.has(id)) {
|
|
56
121
|
tracked.delete(id);
|
|
57
122
|
killed.delete(id);
|
|
123
|
+
opts.cloud?.forgetWorkspace?.(id);
|
|
124
|
+
attempts?.delete(id);
|
|
58
125
|
}
|
|
59
126
|
}
|
|
60
127
|
const reported = [];
|
|
128
|
+
const outcomes = new Map();
|
|
61
129
|
for (const id of completed) {
|
|
130
|
+
if (opts.cloud) {
|
|
131
|
+
const push = opts.cloud.pushWorkspace(id);
|
|
132
|
+
const code = opts.cloud.readExitCode(id);
|
|
133
|
+
const branchNote = (push.pushed
|
|
134
|
+
? `branch ${push.branch ?? 'unknown'} pushed`
|
|
135
|
+
: push.reason === 'no workspace'
|
|
136
|
+
? 'no workspace'
|
|
137
|
+
: `push failed: ${push.reason ?? 'unknown'}`).slice(0, 200);
|
|
138
|
+
if (code === 0 && (push.pushed || push.kind === 'no-workspace')) {
|
|
139
|
+
const ok = await postCompletion(baseUrl, pat, id, opts.fetchImpl);
|
|
140
|
+
if (!ok) {
|
|
141
|
+
noteFailedReport(id);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
attempts?.delete(id);
|
|
145
|
+
tracked.delete(id);
|
|
146
|
+
reported.push(id);
|
|
147
|
+
outcomes.set(id, 'completed');
|
|
148
|
+
const card = opts.renderComplete?.(id);
|
|
149
|
+
opts.log?.(card && card.length > 0 ? card : `✓ ${id} · run completed (pane closed)\n`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const codeLabel = code === null
|
|
153
|
+
? 'unknown (no exit file)'
|
|
154
|
+
: code === TIMEOUT_EXIT_CODE
|
|
155
|
+
? `${TIMEOUT_EXIT_CODE} (wall clock timeout)`
|
|
156
|
+
: String(code);
|
|
157
|
+
const ok = await postFailure(baseUrl, pat, id, `agent exited ${codeLabel} · ${branchNote}`, opts.fetchImpl);
|
|
158
|
+
if (!ok) {
|
|
159
|
+
noteFailedReport(id);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
attempts?.delete(id);
|
|
163
|
+
tracked.delete(id);
|
|
164
|
+
reported.push(id);
|
|
165
|
+
outcomes.set(id, 'failed');
|
|
166
|
+
opts.log?.(`✗ ${id} · run failed (exit ${codeLabel})\n`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const localCode = opts.readExitCode ? opts.readExitCode(id) : null;
|
|
170
|
+
if (localCode !== null && localCode !== 0) {
|
|
171
|
+
const label = localCode === TIMEOUT_EXIT_CODE
|
|
172
|
+
? `${TIMEOUT_EXIT_CODE} (wall clock timeout)`
|
|
173
|
+
: String(localCode);
|
|
174
|
+
const failedOk = await postFailure(baseUrl, pat, id, `agent exited ${label}`, opts.fetchImpl);
|
|
175
|
+
if (!failedOk)
|
|
176
|
+
continue;
|
|
177
|
+
tracked.delete(id);
|
|
178
|
+
reported.push(id);
|
|
179
|
+
outcomes.set(id, 'failed');
|
|
180
|
+
opts.log?.(`✗ ${id} · run failed (exit ${label})\n`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
62
183
|
const ok = await postCompletion(baseUrl, pat, id, opts.fetchImpl);
|
|
63
184
|
if (!ok) {
|
|
64
185
|
continue;
|
|
@@ -68,5 +189,8 @@ export async function checkCompletions(opts) {
|
|
|
68
189
|
const card = opts.renderComplete?.(id);
|
|
69
190
|
opts.log?.(card && card.length > 0 ? card : `✓ ${id} · run completed (pane closed)\n`);
|
|
70
191
|
}
|
|
71
|
-
|
|
192
|
+
const result = reported;
|
|
193
|
+
if (opts.cloud || opts.readExitCode)
|
|
194
|
+
result.outcomes = outcomes;
|
|
195
|
+
return result;
|
|
72
196
|
}
|
|
@@ -9,6 +9,21 @@ function configDir() {
|
|
|
9
9
|
return override;
|
|
10
10
|
return path.join(homedir(), '.config', 'nonbot');
|
|
11
11
|
}
|
|
12
|
+
export function shutdownIssuedAt(signal) {
|
|
13
|
+
if (!signal)
|
|
14
|
+
return null;
|
|
15
|
+
for (const v of [signal.requestedAt, signal.issuedAt]) {
|
|
16
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export function shutdownPredatesDaemon(signal, daemonStartedAt) {
|
|
22
|
+
const issuedAt = shutdownIssuedAt(signal);
|
|
23
|
+
if (issuedAt === null)
|
|
24
|
+
return false;
|
|
25
|
+
return issuedAt < daemonStartedAt;
|
|
26
|
+
}
|
|
12
27
|
export function pidFilePath(dir = configDir()) {
|
|
13
28
|
return path.join(dir, 'daemon.pid');
|
|
14
29
|
}
|
|
@@ -106,6 +121,35 @@ export async function requestRemoteDaemonStop(opts) {
|
|
|
106
121
|
return 1;
|
|
107
122
|
}
|
|
108
123
|
}
|
|
124
|
+
export function attachDaemonSession(deps) {
|
|
125
|
+
const spawn = deps.spawnImpl ?? nodeSpawnSync;
|
|
126
|
+
const sessionName = deps.sessionName ?? 'nonbot';
|
|
127
|
+
try {
|
|
128
|
+
const probe = spawn('tmux', ['has-session', '-t', sessionName], {
|
|
129
|
+
encoding: 'utf-8', timeout: 2000, windowsHide: true,
|
|
130
|
+
});
|
|
131
|
+
if (probe.status !== 0) {
|
|
132
|
+
deps.errLog(`✗ no tmux session '${sessionName}' to attach to.\n Start the daemon first: nonbot daemon\n`);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
deps.errLog('✗ tmux not found — install it (brew install tmux) or run the daemon without tmux.\n');
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
deps.log(`↳ attaching to tmux session '${sessionName}' — press Ctrl-B then D to detach again.\n`);
|
|
141
|
+
try {
|
|
142
|
+
const res = spawn('tmux', ['attach', '-t', sessionName], {
|
|
143
|
+
...{ stdio: 'inherit' },
|
|
144
|
+
windowsHide: true,
|
|
145
|
+
});
|
|
146
|
+
return res.status === 0 ? 0 : 1;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
deps.errLog(`✗ could not attach to '${sessionName}'.\n`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
109
153
|
export async function stopDaemonLocally(deps) {
|
|
110
154
|
const kill = deps.kill ?? ((pid, sig) => process.kill(pid, sig));
|
|
111
155
|
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { captureSnapshotPane, scrubSnapshot, clampSnapshotTail, SNAPSHOT_MAX_BYTES, } from './snapshot.js';
|
|
2
|
+
import { appendActivityLog } from './activity-log.js';
|
|
3
|
+
export const EXIT_TRANSCRIPT_KIND = 'exit-transcript';
|
|
4
|
+
export const EXIT_TRANSCRIPT_LINES = 200;
|
|
5
|
+
export const EXIT_TRANSCRIPT_MAX_BYTES = SNAPSHOT_MAX_BYTES;
|
|
6
|
+
export const EXIT_TRANSCRIPT_OPT_OUT_ENV = 'NONBOT_NO_EXIT_TRANSCRIPT';
|
|
7
|
+
export function exitTranscriptDisabled(env = process.env) {
|
|
8
|
+
const v = env[EXIT_TRANSCRIPT_OPT_OUT_ENV];
|
|
9
|
+
if (typeof v !== 'string')
|
|
10
|
+
return false;
|
|
11
|
+
const t = v.trim().toLowerCase();
|
|
12
|
+
return t === '1' || t === 'true';
|
|
13
|
+
}
|
|
14
|
+
export function refreshPaneTails(opts) {
|
|
15
|
+
const now = opts.now ?? Date.now;
|
|
16
|
+
const maxLines = opts.maxLines ?? EXIT_TRANSCRIPT_LINES;
|
|
17
|
+
let refreshed = 0;
|
|
18
|
+
for (const [activationId, paneId] of opts.tracked) {
|
|
19
|
+
let raw = '';
|
|
20
|
+
try {
|
|
21
|
+
raw = captureSnapshotPane(paneId, maxLines, opts.spawnImpl);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
raw = '';
|
|
25
|
+
}
|
|
26
|
+
if (raw === '')
|
|
27
|
+
continue;
|
|
28
|
+
const { text, truncated } = clampSnapshotTail(scrubSnapshot(raw), EXIT_TRANSCRIPT_MAX_BYTES);
|
|
29
|
+
opts.tails.set(activationId, {
|
|
30
|
+
text,
|
|
31
|
+
truncated,
|
|
32
|
+
capturedAt: now(),
|
|
33
|
+
lines: text.length === 0 ? 0 : text.split('\n').length,
|
|
34
|
+
});
|
|
35
|
+
refreshed++;
|
|
36
|
+
}
|
|
37
|
+
return refreshed;
|
|
38
|
+
}
|
|
39
|
+
export function buildExitTranscriptEntry(opts) {
|
|
40
|
+
return {
|
|
41
|
+
ts: opts.ts,
|
|
42
|
+
id: opts.id,
|
|
43
|
+
kind: EXIT_TRANSCRIPT_KIND,
|
|
44
|
+
status: opts.status,
|
|
45
|
+
profile: opts.profile,
|
|
46
|
+
capturedAt: opts.tail.capturedAt,
|
|
47
|
+
lines: opts.tail.lines,
|
|
48
|
+
truncated: opts.tail.truncated,
|
|
49
|
+
tail: opts.tail.text,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function flushExitTranscripts(opts) {
|
|
53
|
+
const now = opts.now ?? Date.now;
|
|
54
|
+
const append = opts.append ?? ((entry) => appendActivityLog(entry));
|
|
55
|
+
const written = [];
|
|
56
|
+
for (const [id, tail] of [...opts.tails.entries()]) {
|
|
57
|
+
if (opts.tracked.has(id))
|
|
58
|
+
continue;
|
|
59
|
+
const status = opts.statusFor(id);
|
|
60
|
+
const entry = buildExitTranscriptEntry({ id, tail, status, profile: opts.profile, ts: now() });
|
|
61
|
+
try {
|
|
62
|
+
await append(entry);
|
|
63
|
+
written.push(id);
|
|
64
|
+
opts.log?.(`· ${id} · exit transcript saved (${entry.lines} lines${entry.truncated ? ', clamped' : ''} · ${status}) → activations.log\n`);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
opts.log?.(`⚠ ${id} · exit transcript not saved (activations.log write failed)\n`);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
opts.tails.delete(id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return written;
|
|
74
|
+
}
|
package/dist/lib/machine.js
CHANGED
|
@@ -13,6 +13,13 @@ export function machineFilePath(dir = configDir()) {
|
|
|
13
13
|
return path.join(dir, 'machine.json');
|
|
14
14
|
}
|
|
15
15
|
export function loadOrCreateMachineId(deps = {}) {
|
|
16
|
+
const env = deps.env ?? process.env;
|
|
17
|
+
const fromEnv = env.NONBOT_MACHINE_ID;
|
|
18
|
+
if (typeof fromEnv === 'string') {
|
|
19
|
+
const trimmed = fromEnv.trim();
|
|
20
|
+
if (trimmed.length > 0)
|
|
21
|
+
return trimmed.slice(0, 128);
|
|
22
|
+
}
|
|
16
23
|
const fs = deps.fs ?? nodeFs;
|
|
17
24
|
const uuid = deps.uuid ?? randomUUID;
|
|
18
25
|
const dir = deps.dir ?? configDir();
|
|
@@ -14,6 +14,8 @@ const STORY_TITLE_MAX = 200;
|
|
|
14
14
|
const AGENTS_MD_MAX = 24 * 1024;
|
|
15
15
|
const REPO_PATH_MAX = 1024;
|
|
16
16
|
const AGENTS_MD_HEREDOC_DELIMITER = 'NONBOT_AGENTS_EOF';
|
|
17
|
+
const REPO_URL_MAX = 512;
|
|
18
|
+
const REPO_REF_RE = /^[A-Za-z0-9._/-]{1,128}$/;
|
|
17
19
|
const THEME_ID_MAX = 128;
|
|
18
20
|
const THEME_NAME_MAX = 128;
|
|
19
21
|
const THEME_TMUX_CONF_MAX = 8 * 1024;
|
|
@@ -50,6 +52,55 @@ export function validateRepoPath(s, opts = {}) {
|
|
|
50
52
|
}
|
|
51
53
|
return s;
|
|
52
54
|
}
|
|
55
|
+
export function validateRepoUrl(s) {
|
|
56
|
+
if (s === undefined || s === null || s === '')
|
|
57
|
+
return undefined;
|
|
58
|
+
if (typeof s !== 'string') {
|
|
59
|
+
throw new ValidationError('repoUrl', 'must be a string');
|
|
60
|
+
}
|
|
61
|
+
if (s.length > REPO_URL_MAX) {
|
|
62
|
+
throw new ValidationError('repoUrl', `exceeds ${REPO_URL_MAX} chars`);
|
|
63
|
+
}
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = new URL(s);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new ValidationError('repoUrl', 'must be a parseable absolute URL');
|
|
70
|
+
}
|
|
71
|
+
if (parsed.protocol !== 'https:') {
|
|
72
|
+
throw new ValidationError('repoUrl', 'must use https');
|
|
73
|
+
}
|
|
74
|
+
if (parsed.username || parsed.password) {
|
|
75
|
+
throw new ValidationError('repoUrl', 'must not embed credentials');
|
|
76
|
+
}
|
|
77
|
+
if (/[\s'"`;|&<>\\\n\0$]/.test(s)) {
|
|
78
|
+
throw new ValidationError('repoUrl', 'contains shell metacharacters');
|
|
79
|
+
}
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
export function repoUrlLooksSafe(url) {
|
|
83
|
+
if (typeof url !== 'string' || url.length === 0)
|
|
84
|
+
return false;
|
|
85
|
+
try {
|
|
86
|
+
validateRepoUrl(url);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function validateRepoRef(s) {
|
|
94
|
+
if (s === undefined || s === null || s === '')
|
|
95
|
+
return undefined;
|
|
96
|
+
if (typeof s !== 'string') {
|
|
97
|
+
throw new ValidationError('repoRef', 'must be a string');
|
|
98
|
+
}
|
|
99
|
+
if (!REPO_REF_RE.test(s)) {
|
|
100
|
+
throw new ValidationError('repoRef', `must match /^[A-Za-z0-9._/-]{1,128}$/ (got ${JSON.stringify(s.slice(0, 64))})`);
|
|
101
|
+
}
|
|
102
|
+
return s;
|
|
103
|
+
}
|
|
53
104
|
export function validateStoryTitle(s) {
|
|
54
105
|
if (s === undefined || s === null)
|
|
55
106
|
return undefined;
|
package/dist/lib/run-prompt.js
CHANGED
|
@@ -112,6 +112,24 @@ export async function reportPrompt(args) {
|
|
|
112
112
|
return false;
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
export function normalizeAnsweredPrompts(raw) {
|
|
116
|
+
if (!Array.isArray(raw))
|
|
117
|
+
return [];
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const r of raw.slice(0, ANSWERED_MAX)) {
|
|
120
|
+
if (!r || typeof r.promptId !== 'string' || typeof r.activationId !== 'string')
|
|
121
|
+
continue;
|
|
122
|
+
out.push({
|
|
123
|
+
promptId: r.promptId,
|
|
124
|
+
activationId: r.activationId,
|
|
125
|
+
paneId: typeof r.paneId === 'string' && r.paneId.length > 0 ? r.paneId : null,
|
|
126
|
+
answerIndex: typeof r.answerIndex === 'number' ? r.answerIndex : null,
|
|
127
|
+
answerText: typeof r.answerText === 'string' ? r.answerText.slice(0, ANSWER_TEXT_MAX) : null,
|
|
128
|
+
answeredAt: typeof r.answeredAt === 'number' ? r.answeredAt : undefined,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
115
133
|
export async function pollAnsweredPrompts(args) {
|
|
116
134
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
117
135
|
try {
|
|
@@ -126,22 +144,9 @@ export async function pollAnsweredPrompts(args) {
|
|
|
126
144
|
if (!res.ok)
|
|
127
145
|
return [];
|
|
128
146
|
const data = (await res.json());
|
|
129
|
-
if (!data
|
|
147
|
+
if (!data)
|
|
130
148
|
return [];
|
|
131
|
-
|
|
132
|
-
for (const r of data.prompts.slice(0, ANSWERED_MAX)) {
|
|
133
|
-
if (!r || typeof r.promptId !== 'string' || typeof r.activationId !== 'string')
|
|
134
|
-
continue;
|
|
135
|
-
out.push({
|
|
136
|
-
promptId: r.promptId,
|
|
137
|
-
activationId: r.activationId,
|
|
138
|
-
paneId: typeof r.paneId === 'string' && r.paneId.length > 0 ? r.paneId : null,
|
|
139
|
-
answerIndex: typeof r.answerIndex === 'number' ? r.answerIndex : null,
|
|
140
|
-
answerText: typeof r.answerText === 'string' ? r.answerText.slice(0, ANSWER_TEXT_MAX) : null,
|
|
141
|
-
answeredAt: typeof r.answeredAt === 'number' ? r.answeredAt : undefined,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
return out;
|
|
149
|
+
return normalizeAnsweredPrompts(data.prompts);
|
|
145
150
|
}
|
|
146
151
|
catch {
|
|
147
152
|
return [];
|
|
@@ -166,12 +171,28 @@ export async function confirmDelivered(args) {
|
|
|
166
171
|
return false;
|
|
167
172
|
}
|
|
168
173
|
}
|
|
174
|
+
const ANSI_CSI_RE = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
175
|
+
const ANSI_CSI8_RE = /\x9B[0-?]*[ -/]*[@-~]/g;
|
|
176
|
+
const ANSI_OSC_RE = /\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)?/g;
|
|
177
|
+
const ANSI_ESC2_RE = /\x1B[@-~]/g;
|
|
178
|
+
const CONTROL_BYTE_RE = /[\x00-\x1F\x7F-\x9F]/g;
|
|
179
|
+
export function sanitizeAnswerText(text) {
|
|
180
|
+
if (typeof text !== 'string')
|
|
181
|
+
return '';
|
|
182
|
+
return text
|
|
183
|
+
.replace(ANSI_CSI_RE, '')
|
|
184
|
+
.replace(ANSI_CSI8_RE, '')
|
|
185
|
+
.replace(ANSI_OSC_RE, '')
|
|
186
|
+
.replace(ANSI_ESC2_RE, '')
|
|
187
|
+
.replace(CONTROL_BYTE_RE, ' ')
|
|
188
|
+
.trim();
|
|
189
|
+
}
|
|
169
190
|
export function injectAnswer(paneId, answerIndex, answerText, spawnImpl = nodeSpawnSync) {
|
|
170
191
|
if (!PANE_ID_RE.test(paneId))
|
|
171
192
|
return false;
|
|
172
193
|
const keys = answerIndex !== null && Number.isFinite(answerIndex) && answerIndex >= 0
|
|
173
194
|
? String(answerIndex)
|
|
174
|
-
: answerText ?? '';
|
|
195
|
+
: sanitizeAnswerText(answerText ?? '');
|
|
175
196
|
if (keys.length === 0)
|
|
176
197
|
return false;
|
|
177
198
|
try {
|
|
@@ -192,7 +213,7 @@ export function injectAnswer(paneId, answerIndex, answerText, spawnImpl = nodeSp
|
|
|
192
213
|
}
|
|
193
214
|
}
|
|
194
215
|
export async function deliverAnsweredPrompts(opts) {
|
|
195
|
-
const answered = await pollAnsweredPrompts({
|
|
216
|
+
const answered = opts.prompts ?? await pollAnsweredPrompts({
|
|
196
217
|
baseUrl: opts.baseUrl,
|
|
197
218
|
pat: opts.pat,
|
|
198
219
|
machineId: opts.machineId,
|
package/dist/lib/snapshot.js
CHANGED
|
@@ -27,22 +27,24 @@ export function captureSnapshotPane(paneId, maxLines = MAX_LINES_DEFAULT, spawnI
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
const PAT_TOKEN_RE = /pat_[A-Za-z0-9_-]{8,}/g;
|
|
30
|
-
const
|
|
30
|
+
const ANTHROPIC_KEY_RE = /sk-ant-[A-Za-z0-9_-]{8,}/g;
|
|
31
|
+
const GITHUB_TOKEN_RE = /(?:gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,})/g;
|
|
32
|
+
const ENV_ASSIGN_LINE_RE = /(?:^|\s|export )\w*(?:NONBOT_|TOKEN|API_KEY|SECRET)\w*=/;
|
|
31
33
|
const AUTH_BEARER_RE = /Authorization: Bearer \S+/g;
|
|
34
|
+
export function scrubLine(line) {
|
|
35
|
+
if (ENV_ASSIGN_LINE_RE.test(line) || line.includes('NONBOT_PAT')) {
|
|
36
|
+
return '[redacted env line]';
|
|
37
|
+
}
|
|
38
|
+
return line
|
|
39
|
+
.replace(GITHUB_TOKEN_RE, '[REDACTED-GIT-TOKEN]')
|
|
40
|
+
.replace(PAT_TOKEN_RE, 'pat_[REDACTED]')
|
|
41
|
+
.replace(ANTHROPIC_KEY_RE, 'sk-ant-[REDACTED]')
|
|
42
|
+
.replace(AUTH_BEARER_RE, 'Authorization: Bearer [REDACTED]');
|
|
43
|
+
}
|
|
32
44
|
export function scrubSnapshot(text) {
|
|
33
45
|
if (typeof text !== 'string' || text.length === 0)
|
|
34
46
|
return '';
|
|
35
|
-
return text
|
|
36
|
-
.split('\n')
|
|
37
|
-
.map((line) => {
|
|
38
|
-
if (ENV_EXPORT_LINE_RE.test(line) || line.includes('NONBOT_PAT')) {
|
|
39
|
-
return '[redacted env line]';
|
|
40
|
-
}
|
|
41
|
-
return line
|
|
42
|
-
.replace(PAT_TOKEN_RE, 'pat_[REDACTED]')
|
|
43
|
-
.replace(AUTH_BEARER_RE, 'Authorization: Bearer [REDACTED]');
|
|
44
|
-
})
|
|
45
|
-
.join('\n');
|
|
47
|
+
return text.split('\n').map(scrubLine).join('\n');
|
|
46
48
|
}
|
|
47
49
|
export function clampSnapshotTail(text, maxBytes = SNAPSHOT_MAX_BYTES) {
|
|
48
50
|
const buf = Buffer.from(text, 'utf-8');
|
package/dist/lib/terminal.js
CHANGED
|
@@ -23,6 +23,10 @@ function toOsascriptArgs(lines) {
|
|
|
23
23
|
export const TMUX_PRESS_KEY_TRAILER = "; printf '\\n%s\\n' 'Run finished — press enter to close'" +
|
|
24
24
|
'; read _' +
|
|
25
25
|
'; tmux kill-pane';
|
|
26
|
+
export function tmuxCloudTrailer(exitFile) {
|
|
27
|
+
const safe = exitFile.replace(/'/g, `'\\''`);
|
|
28
|
+
return `; echo $? > '${safe}'`;
|
|
29
|
+
}
|
|
26
30
|
export const TERMINAL_PROFILES = [
|
|
27
31
|
{
|
|
28
32
|
id: 'terminal',
|
|
@@ -123,8 +127,9 @@ export const TERMINAL_PROFILES = [
|
|
|
123
127
|
id: 'tmux',
|
|
124
128
|
displayName: 'tmux pane',
|
|
125
129
|
platform: 'darwin',
|
|
126
|
-
launch: (s) => {
|
|
130
|
+
launch: (s, opts) => {
|
|
127
131
|
const safe = s.replace(/'/g, `'\\''`);
|
|
132
|
+
const trailer = opts?.cloudExitFile ? tmuxCloudTrailer(opts.cloudExitFile) : TMUX_PRESS_KEY_TRAILER;
|
|
128
133
|
return {
|
|
129
134
|
cmd: 'tmux',
|
|
130
135
|
args: [
|
|
@@ -132,7 +137,7 @@ export const TERMINAL_PROFILES = [
|
|
|
132
137
|
'-P',
|
|
133
138
|
'-F',
|
|
134
139
|
'#{pane_id}',
|
|
135
|
-
`bash '${safe}' 2>&1${
|
|
140
|
+
`bash '${safe}' 2>&1${trailer}`,
|
|
136
141
|
';',
|
|
137
142
|
'select-layout',
|
|
138
143
|
'tiled',
|
|
@@ -186,8 +191,9 @@ export const TERMINAL_PROFILES = [
|
|
|
186
191
|
id: 'tmux',
|
|
187
192
|
displayName: 'tmux pane',
|
|
188
193
|
platform: 'linux',
|
|
189
|
-
launch: (s) => {
|
|
194
|
+
launch: (s, opts) => {
|
|
190
195
|
const safe = s.replace(/'/g, `'\\''`);
|
|
196
|
+
const trailer = opts?.cloudExitFile ? tmuxCloudTrailer(opts.cloudExitFile) : TMUX_PRESS_KEY_TRAILER;
|
|
191
197
|
return {
|
|
192
198
|
cmd: 'tmux',
|
|
193
199
|
args: [
|
|
@@ -195,7 +201,7 @@ export const TERMINAL_PROFILES = [
|
|
|
195
201
|
'-P',
|
|
196
202
|
'-F',
|
|
197
203
|
'#{pane_id}',
|
|
198
|
-
`bash '${safe}' 2>&1${
|
|
204
|
+
`bash '${safe}' 2>&1${trailer}`,
|
|
199
205
|
';',
|
|
200
206
|
'select-layout',
|
|
201
207
|
'tiled',
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { VERSION } from '../version.js';
|
|
2
|
+
export const NPM_LATEST_URL = 'https://registry.npmjs.org/@nonbot/cli/latest';
|
|
3
|
+
export const UPDATE_CHECK_TIMEOUT_MS = 3_000;
|
|
4
|
+
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60_000;
|
|
5
|
+
export const UPDATE_CHECK_OPT_OUT_ENV = 'NONBOT_NO_UPDATE_CHECK';
|
|
6
|
+
export function updateCheckDisabled(env = process.env) {
|
|
7
|
+
const v = env[UPDATE_CHECK_OPT_OUT_ENV];
|
|
8
|
+
if (typeof v !== 'string')
|
|
9
|
+
return false;
|
|
10
|
+
const t = v.trim().toLowerCase();
|
|
11
|
+
return t === '1' || t === 'true';
|
|
12
|
+
}
|
|
13
|
+
export function parseSemver(v) {
|
|
14
|
+
if (typeof v !== 'string')
|
|
15
|
+
return null;
|
|
16
|
+
const m = /^v?(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:[-+][0-9A-Za-z.-]{0,64})?$/.exec(v.trim());
|
|
17
|
+
if (!m)
|
|
18
|
+
return null;
|
|
19
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
20
|
+
}
|
|
21
|
+
export function compareVersions(a, b) {
|
|
22
|
+
const pa = parseSemver(a);
|
|
23
|
+
const pb = parseSemver(b);
|
|
24
|
+
if (!pa || !pb)
|
|
25
|
+
return 0;
|
|
26
|
+
for (let i = 0; i < 3; i++) {
|
|
27
|
+
if (pa[i] > pb[i])
|
|
28
|
+
return 1;
|
|
29
|
+
if (pa[i] < pb[i])
|
|
30
|
+
return -1;
|
|
31
|
+
}
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
export function isNewerVersion(latest, current) {
|
|
35
|
+
return compareVersions(latest, current) > 0;
|
|
36
|
+
}
|
|
37
|
+
export function updateAvailableLine(current, latest) {
|
|
38
|
+
return `Update available: @nonbot/cli ${current} → ${latest} (npm i -g @nonbot/cli)`;
|
|
39
|
+
}
|
|
40
|
+
export async function fetchLatestVersion(opts = {}) {
|
|
41
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetchImpl(opts.url ?? NPM_LATEST_URL, {
|
|
44
|
+
method: 'GET',
|
|
45
|
+
headers: { Accept: 'application/json' },
|
|
46
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS),
|
|
47
|
+
});
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
return null;
|
|
50
|
+
const body = (await res.json());
|
|
51
|
+
const version = body?.version;
|
|
52
|
+
return parseSemver(version) ? String(version).trim() : null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function createUpdateChecker(opts) {
|
|
59
|
+
const current = opts.current ?? VERSION;
|
|
60
|
+
const env = opts.env ?? process.env;
|
|
61
|
+
const now = opts.now ?? Date.now;
|
|
62
|
+
const intervalMs = opts.intervalMs ?? UPDATE_CHECK_INTERVAL_MS;
|
|
63
|
+
const fetchImpl = opts.fetchImpl === undefined ? fetch : opts.fetchImpl;
|
|
64
|
+
let lastCheckedAt = null;
|
|
65
|
+
let latestSeen = null;
|
|
66
|
+
return {
|
|
67
|
+
get lastCheckedAt() {
|
|
68
|
+
return lastCheckedAt;
|
|
69
|
+
},
|
|
70
|
+
get latestSeen() {
|
|
71
|
+
return latestSeen;
|
|
72
|
+
},
|
|
73
|
+
async maybeCheck() {
|
|
74
|
+
if (fetchImpl === null || updateCheckDisabled(env))
|
|
75
|
+
return false;
|
|
76
|
+
const t = now();
|
|
77
|
+
if (lastCheckedAt !== null && t - lastCheckedAt < intervalMs)
|
|
78
|
+
return false;
|
|
79
|
+
lastCheckedAt = t;
|
|
80
|
+
try {
|
|
81
|
+
const latest = await fetchLatestVersion({
|
|
82
|
+
fetchImpl,
|
|
83
|
+
timeoutMs: opts.timeoutMs,
|
|
84
|
+
url: opts.url,
|
|
85
|
+
});
|
|
86
|
+
if (!latest)
|
|
87
|
+
return false;
|
|
88
|
+
latestSeen = latest;
|
|
89
|
+
if (!isNewerVersion(latest, current))
|
|
90
|
+
return false;
|
|
91
|
+
opts.log(updateAvailableLine(current, latest));
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.10.0';
|
package/package.json
CHANGED