@agentpulselabs/cli-plugin-pulse 0.2.0 → 1.1.2
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/README.md +115 -32
- package/package.json +14 -19
- package/src/clients/hub-bridge.mjs +99 -0
- package/src/clients/hub.mjs +324 -0
- package/src/commands/pulse/cutover.js +39 -0
- package/src/commands/pulse/doctor.js +15 -0
- package/src/commands/pulse/hub-write.js +26 -0
- package/src/commands/pulse/import.js +62 -0
- package/src/commands/pulse/install.js +14 -0
- package/src/commands/pulse/log-write.js +26 -0
- package/src/commands/pulse/login.js +20 -0
- package/src/commands/pulse/mirror.js +27 -0
- package/src/commands/pulse/pair.js +38 -0
- package/src/commands/pulse/parity.js +20 -0
- package/src/commands/pulse/restart.js +14 -0
- package/src/commands/pulse/shadow.js +19 -0
- package/src/commands/pulse/start.js +6 -18
- package/src/commands/pulse/status.js +6 -6
- package/src/commands/pulse/stop.js +24 -0
- package/src/daemon/main.js +43 -0
- package/src/hub/core/auth.mjs +80 -0
- package/src/hub/core/changes.mjs +38 -0
- package/src/hub/core/conversations.mjs +121 -0
- package/src/hub/core/db.mjs +263 -0
- package/src/hub/core/edgeIds.mjs +9 -0
- package/src/hub/core/hub.mjs +20 -0
- package/src/hub/core/ids.mjs +39 -0
- package/src/hub/core/leases.mjs +55 -0
- package/src/hub/core/messaging.mjs +322 -0
- package/src/hub/core/registry.mjs +281 -0
- package/src/hub/core/ticks.mjs +58 -0
- package/src/hub/core/wake.mjs +125 -0
- package/src/hub/home.mjs +16 -0
- package/src/hub/hooks/hub-bridge.mjs +167 -0
- package/src/hub/keepawake/agent.mjs +96 -0
- package/src/hub/lib/brief.mjs +133 -0
- package/src/hub/lib/channels.mjs +268 -0
- package/src/hub/lib/chat.mjs +163 -0
- package/src/hub/lib/closure.mjs +92 -0
- package/src/hub/lib/collect-worker.mjs +20 -0
- package/src/hub/lib/collect.mjs +342 -0
- package/src/hub/lib/deliveries.mjs +222 -0
- package/src/hub/lib/git.mjs +93 -0
- package/src/hub/lib/me.mjs +123 -0
- package/src/hub/lib/personas.mjs +143 -0
- package/src/hub/lib/status.mjs +147 -0
- package/src/hub/lib/store.mjs +106 -0
- package/src/hub/lib/titles.mjs +29 -0
- package/src/hub/lib/transcript-feed.mjs +72 -0
- package/src/hub/lib/transcripts.mjs +364 -0
- package/src/hub/lib/writing.mjs +18 -0
- package/src/hub/projections/deliveries.mjs +31 -0
- package/src/hub/projections/graph.mjs +149 -0
- package/src/hub/runners/drivers.mjs +162 -0
- package/src/hub/workers/collector.mjs +128 -0
- package/src/hub/workers/manager.mjs +366 -0
- package/src/hub/workers/runner.mjs +145 -0
- package/src/lib/BaseCommand.js +23 -23
- package/src/lib/agentSessions.js +279 -0
- package/src/lib/application.js +704 -0
- package/src/lib/backoff.js +8 -0
- package/src/lib/budgets.js +85 -0
- package/src/lib/channelScanner.js +99 -0
- package/src/lib/cliDefaults.js +20 -0
- package/src/lib/clientInstaller.js +111 -0
- package/src/lib/cloudAuth.js +189 -0
- package/src/lib/cloudFirstCommands.js +293 -0
- package/src/lib/contract.sha256 +2 -0
- package/src/lib/cutover.js +376 -0
- package/src/lib/cutoverWiring.js +137 -0
- package/src/lib/daemonComposition.js +338 -0
- package/src/lib/daemonLock.js +57 -0
- package/src/lib/daemonLoop.js +72 -0
- package/src/lib/daemonProcess.js +380 -0
- package/src/lib/daemonRuntime.js +828 -0
- package/src/lib/doctorWarnings.js +53 -0
- package/src/lib/drivers.js +280 -0
- package/src/lib/errors.js +33 -0
- package/src/lib/graphWindows.js +149 -0
- package/src/lib/http.js +84 -0
- package/src/lib/hubActions.js +74 -0
- package/src/lib/hubDbReader.js +132 -0
- package/src/lib/hubMapping.js +108 -0
- package/src/lib/hubScan.js +611 -0
- package/src/lib/hubSnapshot.js +64 -0
- package/src/lib/identity.js +102 -0
- package/src/lib/intents.js +590 -0
- package/src/lib/ledger.js +535 -0
- package/src/lib/lifecycle.js +123 -0
- package/src/lib/localHub.js +217 -0
- package/src/lib/localHubWriter.js +69 -0
- package/src/lib/mirror-contract.json +4299 -0
- package/src/lib/mirrorContract.js +513 -0
- package/src/lib/mirrorDaemon.js +127 -0
- package/src/lib/mirrorService.js +365 -0
- package/src/lib/mirrorSource.js +430 -0
- package/src/lib/mirrorSync.js +353 -0
- package/src/lib/nodeFloor.js +19 -0
- package/src/lib/oauthLogin.js +132 -0
- package/src/lib/outboxFlusher.js +75 -0
- package/src/lib/ownerLock.js +99 -0
- package/src/lib/pairedConfig.js +40 -0
- package/src/lib/pairingLifecycle.js +179 -0
- package/src/lib/pathRedaction.js +89 -0
- package/src/lib/planContract.js +141 -0
- package/src/lib/priorityLane.js +80 -0
- package/src/lib/processOwnership.js +252 -0
- package/src/lib/processSupervisor.js +376 -0
- package/src/lib/reconcile.js +316 -0
- package/src/lib/reconcileView.js +129 -0
- package/src/lib/redaction.js +128 -0
- package/src/lib/rootPolicy.js +372 -0
- package/src/lib/rulePacks.js +162 -0
- package/src/lib/runnerStore.js +418 -0
- package/src/lib/secretStore.js +169 -0
- package/src/lib/selfRestart.js +261 -0
- package/src/lib/serviceManager.js +419 -0
- package/src/lib/shadowService.js +435 -0
- package/src/lib/sqlite.js +28 -0
- package/src/lib/storage.js +131 -0
- package/src/lib/subprocess.js +45 -0
- package/src/lib/tokenStore.js +136 -0
- package/src/lib/transport.js +334 -0
- package/src/lib/transportContract.js +40 -0
- package/src/lib/uploadRedactor.js +102 -0
- package/src/runner/applier.js +297 -0
- package/src/runner/bridge.js +289 -0
- package/src/runner/busIdentity.js +499 -0
- package/src/runner/capture.js +103 -0
- package/src/runner/captureMapping.js +21 -0
- package/src/runner/channelDiscipline.js +425 -0
- package/src/runner/deliveryStates.js +220 -0
- package/src/runner/facade.js +876 -0
- package/src/runner/hubCapture.js +160 -0
- package/src/runner/lease.js +46 -0
- package/src/runner/main.js +35 -0
- package/src/runner/render.js +91 -0
- package/src/runner/runnerChild.js +57 -0
- package/src/runner/runnerHub.js +104 -0
- package/src/runner/runnerProcess.js +124 -0
- package/src/runner/runnerSchema.js +51 -0
- package/src/runner/runnerService.js +140 -0
- package/src/runner/spool.js +86 -0
- package/src/runner/spoolInto.js +22 -0
- package/src/runner/statusPage.js +30 -0
- package/LICENSE +0 -21
- package/src/commands/pulse/gate.js +0 -25
- package/src/commands/pulse/next.js +0 -14
- package/src/commands/pulse/submit.js +0 -21
- package/src/commands/pulse/verify.js +0 -21
- package/src/runPulse.js +0 -58
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// agents-hub. Usage:
|
|
3
|
+
// node hub.mjs serve [--port 8890] map UI at http://127.0.0.1:8890 (?demo = scripted mockup)
|
|
4
|
+
// node hub.mjs map [--hours 6] text summary of the same map, for a chat session
|
|
5
|
+
// node hub.mjs request --from <id> --kind platform|script|rule --title "…" [--files a,b]
|
|
6
|
+
// node hub.mjs post --from <id> --to <id|*> --type NOTE --text "…"
|
|
7
|
+
// node hub.mjs wait --for <id> [--timeout 900]
|
|
8
|
+
// node hub.mjs bus publish|ask|listen|reply|register|who durable addressed messages with acknowledgements
|
|
9
|
+
//
|
|
10
|
+
// The Pulse runner's thin client for `hub.mjs`: the same commands, flags, output and exit codes, carried over HTTP to
|
|
11
|
+
// the runner on 127.0.0.1. It opens no database. Writes made while the runner is down go to an append-only spool
|
|
12
|
+
// that the runner imports once, under the id printed here. Node built-ins only, so it runs from any folder.
|
|
13
|
+
import crypto from 'node:crypto';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
const PULSE = /*PULSE_CLIENT_CONFIG*/ null;
|
|
18
|
+
const config = PULSE || JSON.parse(process.env.PULSE_CLIENT_CONFIG || 'null');
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const cmd = args[0];
|
|
22
|
+
const opt = (name, fallback) => { const i = args.indexOf(`--${name}`); return i > 0 && args[i + 1] !== undefined ? args[i + 1] : fallback; };
|
|
23
|
+
const VALUE_OPTIONS = new Set(['id', 'as', 'reply', 'body', 'name', 'reaction', 'topic', 'type', 'ttl-ms', 'role', 'topics', 'parent',
|
|
24
|
+
'timeout', 'for', 'from', 'to', 'text', 'kind', 'title', 'files', 'hours', 'port', 'supersedes', 'minutes'].map((o) => `--${o}`));
|
|
25
|
+
const FLAGS = new Set(['--ack', '--reload', '--all']);
|
|
26
|
+
/**
|
|
27
|
+
* Bare words after the subcommand, e.g. the answer in `bus reply --id m-… --as <me> "<answer>"`. Only known options
|
|
28
|
+
* take a value; after `--` everything is the answer, so text like "--force is needed" stays whole.
|
|
29
|
+
*/
|
|
30
|
+
function positional(from) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (let i = from; i < args.length; i++) {
|
|
33
|
+
if (args[i] === '--') { out.push(...args.slice(i + 1)); break; }
|
|
34
|
+
if (VALUE_OPTIONS.has(args[i])) { i++; continue; }
|
|
35
|
+
if (FLAGS.has(args[i])) continue;
|
|
36
|
+
out.push(args[i]);
|
|
37
|
+
}
|
|
38
|
+
return out.join(' ').trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The hub's own checks (core/ids.mjs at 6b2fb0b), so a write spooled offline is one the runner will accept.
|
|
42
|
+
const TOPIC = /^(agent\/[A-Za-z0-9-]{8,80}|agent\/(manager|user)|stream\/[A-Za-z0-9._-]{1,80}\/[A-Za-z0-9._-]{1,80}|delivery\/[A-Za-z0-9-]{8,80}|role\/[A-Z][A-Z0-9-]{0,30}|manager|user|broadcast)$/;
|
|
43
|
+
const TYPE = /^[A-Z][A-Z0-9 _-]{0,30}$/;
|
|
44
|
+
const ADDRESS = /^([A-Za-z0-9-]{8,80}|manager|user)$/;
|
|
45
|
+
const MESSAGE_ID = /^m-[a-z0-9-]{4,120}$/;
|
|
46
|
+
const BODY_MAX = 4000;
|
|
47
|
+
const DEFAULT_TTL_MS = 24 * 3600e3;
|
|
48
|
+
|
|
49
|
+
/** Held at most this long by the runner; the client waits a little longer, so the runner always answers first. */
|
|
50
|
+
const LISTEN_HOLD_S = 25;
|
|
51
|
+
const LISTEN_MARGIN_MS = 3000;
|
|
52
|
+
const WRITE_TIMEOUT_MS = 8000;
|
|
53
|
+
const READ_TIMEOUT_MS = 8000;
|
|
54
|
+
|
|
55
|
+
const print = (value, pretty = false) => console.log(pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value));
|
|
56
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
57
|
+
const newId = (at = Date.now()) => `m-${at.toString(36)}-${crypto.randomBytes(3).toString('hex')}`;
|
|
58
|
+
const replyId = (id, as) => `m-r-${crypto.createHash('sha256').update(['r', id, as].join('\0')).digest('hex').slice(0, 24)}`;
|
|
59
|
+
/** The Cursor session this command runs in, when it runs in one: the runner lets only that session act for its aliases. */
|
|
60
|
+
const SESSION = ADDRESS.test(String(process.env.CURSOR_CONVERSATION_ID || '')) ? process.env.CURSOR_CONVERSATION_ID : null;
|
|
61
|
+
const withSession = (body) => (SESSION ? { ...body, session: SESSION } : body);
|
|
62
|
+
|
|
63
|
+
/** Thrown for anything that means "the runner did not answer": not listening, timed out, 5xx, or no token to send. */
|
|
64
|
+
class Unreachable extends Error {}
|
|
65
|
+
|
|
66
|
+
function token() {
|
|
67
|
+
try {
|
|
68
|
+
const value = fs.readFileSync(config.tokenFile, 'utf8').trim();
|
|
69
|
+
if (value) return value;
|
|
70
|
+
} catch { /* no token */ }
|
|
71
|
+
throw new Unreachable('no hub token');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function call(method, route, body, timeoutMs) {
|
|
75
|
+
// During a cutover's freeze the client is installed offline: every write spools, nothing reaches any hub.
|
|
76
|
+
if (!config || config.offline) throw new Unreachable(config ? 'offline' : 'not configured');
|
|
77
|
+
let res;
|
|
78
|
+
try {
|
|
79
|
+
res = await fetch(`http://127.0.0.1:${config.port}${route}`, {
|
|
80
|
+
method,
|
|
81
|
+
headers: { authorization: `Bearer ${token()}`, ...(body ? { 'content-type': 'application/json' } : {}) },
|
|
82
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
83
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof Unreachable) throw error;
|
|
87
|
+
throw new Unreachable(error && error.message);
|
|
88
|
+
}
|
|
89
|
+
if (res.status >= 500 && res.status !== 507) throw new Unreachable(`runner answered ${res.status}`);
|
|
90
|
+
let parsed;
|
|
91
|
+
try { parsed = JSON.parse(await res.text()); } catch { throw new Unreachable('runner answered with no JSON'); }
|
|
92
|
+
if (!parsed || typeof parsed !== 'object') throw new Unreachable('runner answered with no JSON');
|
|
93
|
+
return { status: res.status, body: parsed };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One spool line per write: append-only, owner-only, imported once by the runner under the same id. */
|
|
97
|
+
function spool(op, body) {
|
|
98
|
+
const dir = config && config.spoolDir;
|
|
99
|
+
if (!dir) throw new Error('no spool folder');
|
|
100
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
101
|
+
const at = Date.now();
|
|
102
|
+
const day = new Date(at).toISOString().slice(0, 10).replaceAll('-', '');
|
|
103
|
+
const fd = fs.openSync(path.join(dir, `${day}.jsonl`), fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT, 0o600);
|
|
104
|
+
try { fs.writeSync(fd, `${JSON.stringify({ v: 1, at, op, body })}\n`); } finally { fs.closeSync(fd); }
|
|
105
|
+
return at;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What a publish could not reach, on stderr, so it is seen even when stdout is cut short: one line per recipient
|
|
110
|
+
* nobody can read now, or one line when the topic reached nobody. The command still succeeds.
|
|
111
|
+
*/
|
|
112
|
+
function warnUndelivered(answer) {
|
|
113
|
+
if (answer && answer.error === 'E_UNKNOWN_ADDRESS') {
|
|
114
|
+
const near = Array.isArray(answer.didYouMean) ? answer.didYouMean.map((d) => `agent/${d.address}${d.label ? ` (${d.label})` : ''}`) : [];
|
|
115
|
+
console.error(`unknown address: agent/${answer.address} is not a known session id or bound alias; nothing was sent${near.length ? `. Did you mean ${near.join(', ')}?` : ''}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!answer || answer.error || !Array.isArray(answer.delivery)) return;
|
|
119
|
+
if (Array.isArray(answer.to) && !answer.to.length) {
|
|
120
|
+
console.error(`no recipients: ${answer.topic} reaches nobody right now, so nothing was queued`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const hours = Math.round((Number(answer.expiresAt) - Number(answer.at)) / 3600e3) || 24;
|
|
124
|
+
for (const d of answer.delivery) {
|
|
125
|
+
if (d.state !== 'undeliverable') continue;
|
|
126
|
+
if (d.reason === 'offline') {
|
|
127
|
+
console.error(`undeliverable: ${d.to} offline since ${new Date(d.offlineSince).toISOString()} (queued ${hours} h; it is delivered if its session comes back, binds or listens)`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const why = d.reason === 'stale' ? `its session ${d.session} has had no activity for ${d.idleMin} min` : 'has no reader seen from this machine';
|
|
131
|
+
console.error(`undeliverable: ${d.to} ${why} (queued ${hours} h; it is delivered if a session binds or listens)`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A write: answered by the runner when it is up, spooled when it isn't. Prints what the hub would have printed. */
|
|
136
|
+
async function write(route, op, body, offline) {
|
|
137
|
+
try {
|
|
138
|
+
const { status, body: answer } = await call('POST', route, body, WRITE_TIMEOUT_MS);
|
|
139
|
+
print(answer);
|
|
140
|
+
if (status >= 400 || answer.error) process.exitCode = 2;
|
|
141
|
+
if (route === '/bus/publish' || route === '/bus/ack') warnUndelivered(answer);
|
|
142
|
+
return;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (!(error instanceof Unreachable)) throw error;
|
|
145
|
+
}
|
|
146
|
+
let at;
|
|
147
|
+
try { at = spool(op, body); } catch {
|
|
148
|
+
print({ error: 'pulse_offline_spool_failed' });
|
|
149
|
+
process.exitCode = 2;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
print({ ...offline(at), spooled: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function offlineFailure() {
|
|
156
|
+
print({ error: 'pulse_offline' });
|
|
157
|
+
process.exitCode = 2;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function listen() {
|
|
161
|
+
const as = opt('as');
|
|
162
|
+
const deadline = Date.now() + Math.max(0, Number(opt('timeout', 25))) * 1000;
|
|
163
|
+
if (as) {
|
|
164
|
+
try {
|
|
165
|
+
await call('POST', '/bus/register', { id: as, role: opt('role'), name: opt('name'), topics: String(opt('topics', '')).split(',').filter(Boolean), parent: opt('parent') }, WRITE_TIMEOUT_MS);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (!(error instanceof Unreachable)) throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
do {
|
|
171
|
+
const left = Math.max(0, deadline - Date.now());
|
|
172
|
+
if (!as) { await sleep(Math.min(left, 1000)); continue; }
|
|
173
|
+
const hold = Math.min(LISTEN_HOLD_S, Math.ceil(left / 1000));
|
|
174
|
+
try {
|
|
175
|
+
const { body } = await call('POST', '/bus/listen', withSession({ as, timeout: hold, heartbeat: true }), hold * 1000 + LISTEN_MARGIN_MS);
|
|
176
|
+
if (body.error === 'held') { print(body); process.exitCode = 2; return; }
|
|
177
|
+
if (Array.isArray(body.messages) && body.messages.length) { print({ messages: body.messages }); return; }
|
|
178
|
+
if (body.error) await sleep(Math.min(left, 1000));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (!(error instanceof Unreachable)) throw error;
|
|
181
|
+
await sleep(Math.min(left, 1000));
|
|
182
|
+
}
|
|
183
|
+
} while (Date.now() < deadline);
|
|
184
|
+
print({ messages: [], type: 'TIMEOUT' });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function busCli() {
|
|
188
|
+
const action = args[1];
|
|
189
|
+
if (action === 'register') {
|
|
190
|
+
const body = { id: opt('as'), role: opt('role'), name: opt('name'), topics: String(opt('topics', '')).split(',').filter(Boolean), parent: opt('parent') };
|
|
191
|
+
if (!ADDRESS.test(String(body.id || ''))) { print({ error: 'bad address' }); process.exitCode = 2; return; }
|
|
192
|
+
return write('/bus/register', 'register', body, () => ({ id: body.id }));
|
|
193
|
+
}
|
|
194
|
+
if (action === 'publish' || action === 'ask') {
|
|
195
|
+
const at = Date.now();
|
|
196
|
+
const body = {
|
|
197
|
+
id: newId(at), from: opt('as'), fromName: opt('name'), topic: opt('topic'), type: opt('type', action === 'ask' ? 'QUESTION' : 'MESSAGE'),
|
|
198
|
+
body: opt('body') || positional(2), needsAck: action === 'ask' || args.includes('--ack'), ttlMs: Number(opt('ttl-ms')) || undefined,
|
|
199
|
+
...(opt('supersedes') !== undefined ? { supersedes: opt('supersedes') } : {}),
|
|
200
|
+
};
|
|
201
|
+
const refused = !ADDRESS.test(String(body.from || '')) ? 'bad sender' : !TOPIC.test(String(body.topic || '')) ? 'bad topic'
|
|
202
|
+
: !TYPE.test(String(body.type || '')) ? 'bad type' : (typeof body.body !== 'string' || !body.body.trim()) ? 'empty body'
|
|
203
|
+
: (body.supersedes !== undefined && !MESSAGE_ID.test(String(body.supersedes))) ? 'bad supersedes' : null;
|
|
204
|
+
if (refused) { print({ error: refused }); process.exitCode = 2; return; }
|
|
205
|
+
return write('/bus/publish', 'publish', body, (spooledAt) => ({
|
|
206
|
+
id: body.id, at: spooledAt, from: body.from, fromName: typeof body.fromName === 'string' ? body.fromName.slice(0, 80) : null,
|
|
207
|
+
topic: body.topic, type: body.type, body: body.body.slice(0, BODY_MAX), needsAck: !!body.needsAck,
|
|
208
|
+
expiresAt: spooledAt + Math.max(60e3, Math.min(Number(body.ttlMs) || DEFAULT_TTL_MS, 7 * DEFAULT_TTL_MS)),
|
|
209
|
+
replyTo: null, reaction: null, to: [], meta: body.supersedes ? { supersedes: body.supersedes } : {},
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
if (action === 'listen') return listen();
|
|
213
|
+
if (action === 'ack' || action === 'reply') {
|
|
214
|
+
const body = { as: opt('as'), id: opt('id'), reply: opt('reply') || opt('body') || positional(2) || undefined, fromName: opt('name'), reaction: opt('reaction') };
|
|
215
|
+
if (!ADDRESS.test(String(body.as || '')) || !MESSAGE_ID.test(String(body.id || ''))) { print({ error: 'bad ack' }); process.exitCode = 2; return; }
|
|
216
|
+
const wantsReply = (typeof body.reply === 'string' && body.reply.trim()) || body.reaction;
|
|
217
|
+
return write('/bus/ack', 'ack', withSession(body), () => ({ ok: true, reply: wantsReply ? replyId(body.id, body.as) : null }));
|
|
218
|
+
}
|
|
219
|
+
if (action === 'who') {
|
|
220
|
+
// Active sessions only, unless --all (F4); a runner from before 1.1.1 has no /bus/who and answers the old list.
|
|
221
|
+
const query = `all=${args.includes('--all') ? 1 : 0}${opt('minutes') ? `&minutes=${encodeURIComponent(opt('minutes'))}` : ''}`;
|
|
222
|
+
try {
|
|
223
|
+
const who = await call('GET', `/bus/who?${query}`, null, READ_TIMEOUT_MS);
|
|
224
|
+
if (who.status === 404) print({ registrations: (await call('GET', '/bus/directory', null, READ_TIMEOUT_MS)).body.registrations }, true);
|
|
225
|
+
else print(who.body, true);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (!(error instanceof Unreachable)) throw error;
|
|
228
|
+
offlineFailure();
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (action === 'status') {
|
|
233
|
+
const id = opt('id');
|
|
234
|
+
if (!MESSAGE_ID.test(String(id || ''))) { print({ error: 'bad id' }); process.exitCode = 2; return; }
|
|
235
|
+
try {
|
|
236
|
+
const { status, body: answer } = await call('GET', `/bus/status?id=${encodeURIComponent(id)}`, null, READ_TIMEOUT_MS);
|
|
237
|
+
print(answer, true);
|
|
238
|
+
if (status >= 400 || answer.error) process.exitCode = 2;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (!(error instanceof Unreachable)) throw error;
|
|
241
|
+
offlineFailure();
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (action === 'messages') {
|
|
246
|
+
const since = Number(opt('since')) || 0;
|
|
247
|
+
try { print({ messages: (await call('GET', `/bus/messages?since=${since}&limit=200`, null, READ_TIMEOUT_MS)).body.messages }, true); } catch (error) {
|
|
248
|
+
if (!(error instanceof Unreachable)) throw error;
|
|
249
|
+
offlineFailure();
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
console.error('bus: publish|ask|listen|ack|reply|register|who|messages');
|
|
254
|
+
process.exitCode = 2;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function fmtDur(ms) { const m = Math.round(ms / 60e3); return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}`; }
|
|
258
|
+
function fmtTok(n) { return n == null ? '—' : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n); }
|
|
259
|
+
|
|
260
|
+
async function printMap() {
|
|
261
|
+
let g;
|
|
262
|
+
try { g = (await call('GET', `/api/legacy/map?hours=${Number(opt('hours', 6))}`, null, 60e3)).body; } catch (error) {
|
|
263
|
+
if (!(error instanceof Unreachable)) throw error;
|
|
264
|
+
offlineFailure();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const t = g.totals;
|
|
268
|
+
console.log(`Last ${g.hours} h: ${t.sessions} sessions, ${t.agents} agents (${t.active} active), ${t.messages} messages, ≈${fmtTok(t.tokensEst)} tokens (estimate), ${t.requests} shared-change requests\n`);
|
|
269
|
+
const kids = new Map();
|
|
270
|
+
for (const n of g.nodes) if (n.parent) { if (!kids.has(n.parent)) kids.set(n.parent, []); kids.get(n.parent).push(n); }
|
|
271
|
+
const line = (n, pad) => console.log(`${pad}${n.status === 'active' ? '●' : n.status === 'idle' ? '◐' : '○'} ${n.label.padEnd(pad ? 22 : 46)} ${(n.channel || '').padEnd(14)} ${fmtDur(n.lastAt - n.startedAt).padStart(6)} ≈${fmtTok(n.tokensEst).padStart(5)} ${n.msgsOut}↑ ${n.msgsIn}↓`);
|
|
272
|
+
for (const s of g.nodes.filter((n) => n.kind === 'session' && !n.parent).sort((a, b) => b.lastAt - a.lastAt)) {
|
|
273
|
+
line(s, '');
|
|
274
|
+
for (const k of (kids.get(s.id) || []).sort((a, b) => b.lastAt - a.lastAt)) line(k, ' └ ');
|
|
275
|
+
}
|
|
276
|
+
const loose = g.nodes.filter((n) => n.kind === 'agent' && !n.parent);
|
|
277
|
+
if (loose.length) { console.log('\nAgents without a known parent session:'); for (const n of loose) line(n, ' '); }
|
|
278
|
+
if (g.requests.length) { console.log('\nShared-change requests:'); for (const r of g.requests.slice(0, 15)) console.log(` [${r.status}] ${r.title} (${r.channel || r.from.join(',')})`); }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function legacy(route, body) {
|
|
282
|
+
try {
|
|
283
|
+
const { status, body: answer } = await call('POST', route, body, WRITE_TIMEOUT_MS);
|
|
284
|
+
print(answer);
|
|
285
|
+
if (status >= 400 || answer.error) process.exitCode = 2;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (!(error instanceof Unreachable)) throw error;
|
|
288
|
+
offlineFailure();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function wait() {
|
|
293
|
+
const me = opt('for');
|
|
294
|
+
if (!me) { console.error('wait: --for <id> is required'); process.exit(2); }
|
|
295
|
+
const deadline = Date.now() + Number(opt('timeout', 900)) * 1000;
|
|
296
|
+
const started = Date.now();
|
|
297
|
+
while (Date.now() < deadline) {
|
|
298
|
+
try {
|
|
299
|
+
const { body } = await call('POST', '/api/legacy/wait', { for: me, since: started }, WRITE_TIMEOUT_MS);
|
|
300
|
+
if (body.message || body.event) { print(body.message || body.event); return; }
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (!(error instanceof Unreachable)) throw error;
|
|
303
|
+
}
|
|
304
|
+
await sleep(5000);
|
|
305
|
+
}
|
|
306
|
+
print({ type: 'TIMEOUT' });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
switch (cmd) {
|
|
310
|
+
case 'serve':
|
|
311
|
+
console.error('The local Agent Hub is retired on this machine: the Pulse runner serves 127.0.0.1:8890. Undo with `agentpulse pulse cutover --undo`.');
|
|
312
|
+
process.exitCode = 78;
|
|
313
|
+
break;
|
|
314
|
+
case 'map': await printMap(); break;
|
|
315
|
+
case 'request':
|
|
316
|
+
await legacy('/api/legacy/request', { from: opt('from'), kind: opt('kind'), title: opt('title'), files: opt('files') });
|
|
317
|
+
break;
|
|
318
|
+
case 'post':
|
|
319
|
+
await legacy('/api/legacy/post', { from: opt('from'), to: opt('to'), type: opt('type'), text: opt('text') });
|
|
320
|
+
break;
|
|
321
|
+
case 'wait': await wait(); break;
|
|
322
|
+
case 'bus': await busCli(); break;
|
|
323
|
+
default: console.log(fs.readFileSync(new URL(import.meta.url), 'utf8').split('\n').slice(1, 8).map((l) => l.replace(/^\/\/ ?/, '')).join('\n'));
|
|
324
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
3
|
+
import { defaultConfigDirectory } from '../../lib/application.js';
|
|
4
|
+
import { createCutover } from '../../lib/cutoverWiring.js';
|
|
5
|
+
|
|
6
|
+
export default class PulseCutover extends BaseCommand {
|
|
7
|
+
static description = 'Move this machine\'s local Agent Hub into the Pulse daemon (a short freeze, a backup kept), check first with --dry-run, or go back with --rollback (restores the backup; loses only what was written after the switch).';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> pulse cutover --dry-run',
|
|
10
|
+
'<%= config.bin %> pulse cutover --smoke-channel agentpulse-agent-hub/cf-phase3',
|
|
11
|
+
'<%= config.bin %> pulse cutover --rollback',
|
|
12
|
+
];
|
|
13
|
+
static flags = {
|
|
14
|
+
'dry-run': Flags.boolean({ description: 'Check everything and say what would change; change nothing', default: false }),
|
|
15
|
+
rollback: Flags.boolean({ description: 'Back to the local hub from the backup', default: false, exclusive: ['dry-run'] }),
|
|
16
|
+
'smoke-channel': Flags.string({ description: 'A task channel (<repo>/<task>) for the smoke test\'s one channel post' }),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
async run() {
|
|
20
|
+
const { flags } = await this.parse(PulseCutover);
|
|
21
|
+
const cutover = this.cutover || await createCutover({ configDir: defaultConfigDirectory(), log: (event, details) => this.log(`${event} ${JSON.stringify(details)}`) });
|
|
22
|
+
if (flags['dry-run']) {
|
|
23
|
+
const report = await cutover.dryRun();
|
|
24
|
+
for (const check of report.checks) this.log(`${check.ok ? 'ok ' : 'FAIL'} ${check.name}${check.detail ? `: ${check.detail}` : ''}${!check.ok && check.fix ? ` (fix: ${check.fix})` : ''}`);
|
|
25
|
+
this.log(`Outage while it runs: ${report.outage.what}; at most ${Math.round(report.outage.boundMs / 60_000)} min.`);
|
|
26
|
+
this.log(report.message);
|
|
27
|
+
if (!report.ok) process.exitCode = 1;
|
|
28
|
+
return report;
|
|
29
|
+
}
|
|
30
|
+
if (flags.rollback) {
|
|
31
|
+
const report = await cutover.rollback();
|
|
32
|
+
this.log(report.message);
|
|
33
|
+
return report;
|
|
34
|
+
}
|
|
35
|
+
const journal = await cutover.run({ smokeChannel: flags['smoke-channel'] ?? null });
|
|
36
|
+
this.log(`Cut over: the Pulse daemon serves 127.0.0.1:${journal.port}. The backup is ${journal.backup.file}. Go back any time with \`agentpulse pulse cutover --rollback\`.`);
|
|
37
|
+
return journal;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
2
|
+
import { getDefaultApplication } from '../../lib/application.js';
|
|
3
|
+
|
|
4
|
+
export default class PulseDoctor extends BaseCommand {
|
|
5
|
+
static description = 'Diagnose the Pulse companion installation and local providers.';
|
|
6
|
+
static examples = ['<%= config.bin %> pulse doctor'];
|
|
7
|
+
|
|
8
|
+
async run() {
|
|
9
|
+
const { flags } = await this.parse(PulseDoctor);
|
|
10
|
+
const result = await (this.runtime || getDefaultApplication()).doctor(flags);
|
|
11
|
+
this.log(result.message);
|
|
12
|
+
for (const warning of result.warnings || []) this.log(`Warning: ${warning}`);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
3
|
+
import { getDefaultApplication } from '../../lib/application.js';
|
|
4
|
+
|
|
5
|
+
export default class PulseHubWrite extends BaseCommand {
|
|
6
|
+
static description = 'Let Pulse post to the local Agent Hub as you (the hub.write grant), stop it, or show whether it can.';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> pulse hub-write status',
|
|
9
|
+
'<%= config.bin %> pulse hub-write on',
|
|
10
|
+
'<%= config.bin %> pulse hub-write off',
|
|
11
|
+
];
|
|
12
|
+
static args = {
|
|
13
|
+
action: Args.string({
|
|
14
|
+
description: 'on, off or status',
|
|
15
|
+
options: ['on', 'off', 'status'],
|
|
16
|
+
default: 'status',
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async run() {
|
|
21
|
+
const { args } = await this.parse(PulseHubWrite);
|
|
22
|
+
const result = await (this.runtime || getDefaultApplication()).hubWrite({ action: args.action });
|
|
23
|
+
this.log(result.message);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
3
|
+
import { defaultConfigDirectory } from '../../lib/application.js';
|
|
4
|
+
import { CloudFirstCommands } from '../../lib/cloudFirstCommands.js';
|
|
5
|
+
|
|
6
|
+
export default class PulseImport extends BaseCommand {
|
|
7
|
+
static description = 'Import the local Agent Hub into the Pulse log from a snapshot of its sources (idempotent: a re-run on the same snapshot writes nothing). --dry-run reads only and prints what it would send; --reconcile rescans the snapshot and compares it with Pulse, exiting 1 on any mismatch.';
|
|
8
|
+
static examples = ['<%= config.bin %> pulse import --dry-run', '<%= config.bin %> pulse import', '<%= config.bin %> pulse import --reconcile', '<%= config.bin %> pulse import --reuse-snapshot'];
|
|
9
|
+
static flags = {
|
|
10
|
+
'dry-run': Flags.boolean({ description: 'Read the local hub and report per source; send and write nothing', default: false }),
|
|
11
|
+
wait: Flags.boolean({ description: 'Follow the import until it has fully drained', default: true, allowNo: true }),
|
|
12
|
+
reconcile: Flags.boolean({ description: 'Rescan the last import\'s snapshot and compare every family, channel and conversation with Pulse', default: false }),
|
|
13
|
+
'reuse-snapshot': Flags.boolean({ description: 'Import the last snapshot again instead of taking a new one', default: false }),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
async run() {
|
|
17
|
+
const { flags } = await this.parse(PulseImport);
|
|
18
|
+
const commands = this.cloudFirst || new CloudFirstCommands({ configDir: defaultConfigDirectory() });
|
|
19
|
+
if (flags['dry-run']) {
|
|
20
|
+
const result = await commands.dryRun();
|
|
21
|
+
for (const row of result.sources) {
|
|
22
|
+
const detail = [...Object.entries(row.skipped).map(([k, v]) => `skipped ${k} ${v}`), ...Object.entries(row.refused).map(([k, v]) => `refused ${k} ${v}`)];
|
|
23
|
+
this.log(`${row.source.padEnd(9)} read ${row.read} planned ${row.planned} unchanged ${row.unchanged} redactions ${row.redactions}${row.cut ? ` cut ${row.cut}` : ''}${detail.length ? ` (${detail.join(', ')})` : ''}`);
|
|
24
|
+
if (Object.keys(row.events).length) this.log(` events ${Object.entries(row.events).map(([type, n]) => `${type} ${n}`).join(', ')}`);
|
|
25
|
+
}
|
|
26
|
+
this.log(result.message);
|
|
27
|
+
if (!result.totals.balanced) process.exitCode = 1;
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
let last = '';
|
|
31
|
+
if (flags.reconcile) {
|
|
32
|
+
const result = await commands.reconcile({
|
|
33
|
+
onProgress: (progress) => {
|
|
34
|
+
const line = `Reconcile: ${progress.state}${progress.code ? ` (${progress.code})` : ''}`;
|
|
35
|
+
if (line !== last) this.log(line);
|
|
36
|
+
last = line;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
for (const [family, row] of Object.entries(result.families || {})) {
|
|
40
|
+
this.log(`${family.padEnd(18)} local ${String(row.local).padStart(6)} pulse ${String(row.cloud).padStart(6)} ${row.equal ? 'equal' : 'DIFFERS'}`);
|
|
41
|
+
}
|
|
42
|
+
for (const [source, row] of Object.entries(result.sources || {})) {
|
|
43
|
+
this.log(`source ${source.padEnd(9)} rows ${row.counted ?? '—'} read ${row.read} accounted ${row.accounted}`);
|
|
44
|
+
}
|
|
45
|
+
for (const item of (result.mismatches || []).slice(0, 20)) this.log(`MISMATCH ${item.family}${item.item ? ` ${item.item}` : ''}${item.note ? `: ${item.note}` : ''}`);
|
|
46
|
+
this.log(result.message);
|
|
47
|
+
process.exitCode = result.exitCode;
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
const result = await commands.import({
|
|
51
|
+
wait: flags.wait,
|
|
52
|
+
reuseSnapshot: flags['reuse-snapshot'],
|
|
53
|
+
onProgress: (progress) => {
|
|
54
|
+
const line = `Import: ${progress.state}${progress.code ? ` (${progress.code})` : ''}${progress.pending !== null && progress.pending !== undefined ? `, ${progress.pending} commands waiting` : ''}`;
|
|
55
|
+
if (line !== last) this.log(line);
|
|
56
|
+
last = line;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
this.log(result.message);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
2
|
+
import { getDefaultApplication } from '../../lib/application.js';
|
|
3
|
+
|
|
4
|
+
export default class PulseInstall extends BaseCommand {
|
|
5
|
+
static description = 'Install or atomically update the Pulse companion daemon service.';
|
|
6
|
+
static examples = ['<%= config.bin %> pulse install'];
|
|
7
|
+
|
|
8
|
+
async run() {
|
|
9
|
+
const { flags } = await this.parse(PulseInstall);
|
|
10
|
+
const result = await (this.runtime || getDefaultApplication()).install(flags);
|
|
11
|
+
this.log(result.message);
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
3
|
+
import { getDefaultApplication } from '../../lib/application.js';
|
|
4
|
+
|
|
5
|
+
export default class PulseLogWrite extends BaseCommand {
|
|
6
|
+
static description = 'Let this machine read and write your Pulse log (hub.sync and log.write), stop it writing, or show whether it can.';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> pulse log-write status',
|
|
9
|
+
'<%= config.bin %> pulse log-write on',
|
|
10
|
+
'<%= config.bin %> pulse log-write off',
|
|
11
|
+
];
|
|
12
|
+
static args = {
|
|
13
|
+
action: Args.string({
|
|
14
|
+
description: 'on, off or status',
|
|
15
|
+
options: ['on', 'off', 'status'],
|
|
16
|
+
default: 'status',
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async run() {
|
|
21
|
+
const { args } = await this.parse(PulseLogWrite);
|
|
22
|
+
const result = await (this.runtime || getDefaultApplication()).logWrite({ action: args.action });
|
|
23
|
+
this.log(result.message);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
3
|
+
import { getDefaultApplication } from '../../lib/application.js';
|
|
4
|
+
|
|
5
|
+
export default class PulseLogin extends BaseCommand {
|
|
6
|
+
static description = 'Sign the Pulse connector in again with your AgentPulse account (browser OAuth with PKCE).';
|
|
7
|
+
static examples = ['<%= config.bin %> pulse login', '<%= config.bin %> pulse login --platform https://app.agentpulse.example.com'];
|
|
8
|
+
static flags = {
|
|
9
|
+
platform: Flags.string({
|
|
10
|
+
description: 'Platform URL to sign in to (defaults to the paired platform).',
|
|
11
|
+
}),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async run() {
|
|
15
|
+
const { flags } = await this.parse(PulseLogin);
|
|
16
|
+
const result = await (this.runtime || getDefaultApplication()).login({ platformUrl: flags.platform });
|
|
17
|
+
this.log(result.message);
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
3
|
+
import { getDefaultApplication } from '../../lib/application.js';
|
|
4
|
+
|
|
5
|
+
export default class PulseMirror extends BaseCommand {
|
|
6
|
+
static description = 'Turn mirroring of the local Agent Hub into Pulse on or off, show its status, or send the items Pulse refused again.';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> pulse mirror status',
|
|
9
|
+
'<%= config.bin %> pulse mirror on',
|
|
10
|
+
'<%= config.bin %> pulse mirror off',
|
|
11
|
+
'<%= config.bin %> pulse mirror resend-rejected',
|
|
12
|
+
];
|
|
13
|
+
static args = {
|
|
14
|
+
action: Args.string({
|
|
15
|
+
description: 'on, off, status or resend-rejected',
|
|
16
|
+
options: ['on', 'off', 'status', 'resend-rejected'],
|
|
17
|
+
default: 'status',
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async run() {
|
|
22
|
+
const { args } = await this.parse(PulseMirror);
|
|
23
|
+
const result = await (this.runtime || getDefaultApplication()).mirror({ action: args.action });
|
|
24
|
+
this.log(result.message);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
3
|
+
import { getDefaultApplication } from '../../lib/application.js';
|
|
4
|
+
|
|
5
|
+
export default class PulsePair extends BaseCommand {
|
|
6
|
+
static description = 'Pair this machine with Pulse using your AgentPulse sign-in.';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> pulse pair --org builddevorg8 --root agent-hub=~/src/agent-hub',
|
|
9
|
+
'<%= config.bin %> pulse pair --platform https://app.agentpulse.example.com --org my-dev-org --root app=.',
|
|
10
|
+
];
|
|
11
|
+
static flags = {
|
|
12
|
+
org: Flags.string({
|
|
13
|
+
description: 'Dev-org slug that hosts Pulse (defaults to the agentpulse CLI active org).',
|
|
14
|
+
}),
|
|
15
|
+
platform: Flags.string({
|
|
16
|
+
description: 'Platform URL to sign in to (defaults to the agentpulse CLI active profile).',
|
|
17
|
+
}),
|
|
18
|
+
name: Flags.string({
|
|
19
|
+
description: 'Display name for this machine.',
|
|
20
|
+
}),
|
|
21
|
+
root: Flags.string({
|
|
22
|
+
description: 'Approve a local root as <rootId>=<path> (repeatable).',
|
|
23
|
+
multiple: true,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
async run() {
|
|
28
|
+
const { flags } = await this.parse(PulsePair);
|
|
29
|
+
const result = await (this.runtime || getDefaultApplication()).pair({
|
|
30
|
+
org: flags.org,
|
|
31
|
+
platformUrl: flags.platform,
|
|
32
|
+
displayName: flags.name,
|
|
33
|
+
roots: flags.root || [],
|
|
34
|
+
});
|
|
35
|
+
this.log(result.message);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
3
|
+
import { defaultConfigDirectory } from '../../lib/application.js';
|
|
4
|
+
import { CloudFirstCommands } from '../../lib/cloudFirstCommands.js';
|
|
5
|
+
|
|
6
|
+
export default class PulseParity extends BaseCommand {
|
|
7
|
+
static description = 'Show how this machine\'s Pulse mirror and the Pulse log agree: the last parity check and its history, or a fresh one.';
|
|
8
|
+
static examples = ['<%= config.bin %> pulse parity', '<%= config.bin %> pulse parity --now --hours 6'];
|
|
9
|
+
static flags = {
|
|
10
|
+
hours: Flags.integer({ description: 'Window in hours (1 to 168)', default: 6, min: 1, max: 168 }),
|
|
11
|
+
now: Flags.boolean({ description: 'Ask the daemon for a fresh check and wait for it', default: false }),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async run() {
|
|
15
|
+
const { flags } = await this.parse(PulseParity);
|
|
16
|
+
const result = await (this.cloudFirst || new CloudFirstCommands({ configDir: defaultConfigDirectory() })).parity({ hours: flags.hours, now: flags.now });
|
|
17
|
+
this.log(result.message);
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
2
|
+
import { getDefaultApplication } from '../../lib/application.js';
|
|
3
|
+
|
|
4
|
+
export default class PulseRestart extends BaseCommand {
|
|
5
|
+
static description = 'Ask the running Pulse daemon to restart itself and wait for the new process (no launchctl or systemctl needed).';
|
|
6
|
+
static examples = ['<%= config.bin %> pulse restart'];
|
|
7
|
+
|
|
8
|
+
async run() {
|
|
9
|
+
await this.parse(PulseRestart);
|
|
10
|
+
const result = await (this.runtime || getDefaultApplication()).restart();
|
|
11
|
+
this.log(result.message);
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../lib/BaseCommand.js';
|
|
3
|
+
import { defaultConfigDirectory } from '../../lib/application.js';
|
|
4
|
+
import { CloudFirstCommands } from '../../lib/cloudFirstCommands.js';
|
|
5
|
+
|
|
6
|
+
export default class PulseShadow extends BaseCommand {
|
|
7
|
+
static description = 'Turn the Pulse shadow feed (the local hub, tailed into the Pulse log) on or off, or show its status.';
|
|
8
|
+
static examples = ['<%= config.bin %> pulse shadow status', '<%= config.bin %> pulse shadow on', '<%= config.bin %> pulse shadow off'];
|
|
9
|
+
static args = {
|
|
10
|
+
action: Args.string({ description: 'on, off or status', options: ['on', 'off', 'status'], default: 'status' }),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async run() {
|
|
14
|
+
const { args } = await this.parse(PulseShadow);
|
|
15
|
+
const result = await (this.cloudFirst || new CloudFirstCommands({ configDir: defaultConfigDirectory() })).shadow({ action: args.action });
|
|
16
|
+
this.log(result.message);
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
}
|