agentradar-cli 0.3.6 → 0.3.8
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/dist/agent-guard/hooks/hooks.json +7 -7
- package/dist/agent-guard/skills/agentradar-discipline/SKILL.md +6 -0
- package/dist/agentradar-vendor/SKILL.md +1 -1
- package/dist/channel.js +39 -15
- package/dist/index.js +60 -104
- package/dist/setup.js +47 -34
- package/dist/skill-install.js +6 -4
- package/dist/tool-run.js +196 -157
- package/dist/vendor-runner/feeder.mjs +67 -8
- package/dist/vendor-runner/session-frame.mjs +86 -2
- package/dist/vendor-runner/supervisor/supervise-agent.sh +81 -15
- package/dist/vendor-runner/terminal-bridge.mjs +5 -0
- package/package.json +1 -2
- package/dist/commands.js +0 -74
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"description": "Marks the session as guarded (the runner reports ready only then); blocks going idle while AgentRadar work is owed; reminds of the house rules every AR_DISCIPLINE_EVERY turns; reports compactions to AgentRadar; guards AgentRadar compact-then-switch model changes (a crashed PreModelSwitch hook exits 2 and refuses).",
|
|
3
3
|
"hooks": {
|
|
4
|
-
"SessionStart": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" start", "timeout": 10 }] }],
|
|
5
|
-
"Stop": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" stop", "timeout": 30 }] }],
|
|
6
|
-
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" prompt", "timeout": 10 }] }],
|
|
7
|
-
"PreCompact": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PreCompact", "timeout": 30 }, { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/compaction-report.mjs\" PreCompact", "timeout": 30 }] }],
|
|
8
|
-
"PostCompact": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PostCompact", "timeout": 30 }, { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/compaction-report.mjs\" PostCompact", "timeout": 30 }] }],
|
|
9
|
-
"PreModelSwitch": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PreModelSwitch || exit 2", "timeout": 30 }] }],
|
|
10
|
-
"PostModelSwitch": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PostModelSwitch", "timeout": 30 }] }]
|
|
4
|
+
"SessionStart": [{ "hooks": [{ "type": "command", "command": "AR_HOOK=1 node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" start", "timeout": 10 }] }],
|
|
5
|
+
"Stop": [{ "hooks": [{ "type": "command", "command": "AR_HOOK=1 node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" stop", "timeout": 30 }] }],
|
|
6
|
+
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "AR_HOOK=1 node \"${CLAUDE_PLUGIN_ROOT}/scripts/guard.mjs\" prompt", "timeout": 10 }] }],
|
|
7
|
+
"PreCompact": [{ "hooks": [{ "type": "command", "command": "AR_HOOK=1 node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PreCompact", "timeout": 30 }, { "type": "command", "command": "AR_HOOK=1 node \"${CLAUDE_PLUGIN_ROOT}/scripts/compaction-report.mjs\" PreCompact", "timeout": 30 }] }],
|
|
8
|
+
"PostCompact": [{ "hooks": [{ "type": "command", "command": "AR_HOOK=1 node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PostCompact", "timeout": 30 }, { "type": "command", "command": "AR_HOOK=1 node \"${CLAUDE_PLUGIN_ROOT}/scripts/compaction-report.mjs\" PostCompact", "timeout": 30 }] }],
|
|
9
|
+
"PreModelSwitch": [{ "hooks": [{ "type": "command", "command": "AR_HOOK=1 node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PreModelSwitch || exit 2", "timeout": 30 }] }],
|
|
10
|
+
"PostModelSwitch": [{ "hooks": [{ "type": "command", "command": "AR_HOOK=1 node \"${CLAUDE_PLUGIN_ROOT}/scripts/model-switch.mjs\" PostModelSwitch", "timeout": 30 }] }]
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -20,6 +20,12 @@ You act through `agentradar call <runner-id> <tool>`. Your runner id is in `$AR_
|
|
|
20
20
|
|
|
21
21
|
The Stop hook runs the same check and will not let you stop while something is owed.
|
|
22
22
|
|
|
23
|
+
## Hosted workspace lifetime
|
|
24
|
+
|
|
25
|
+
There is no runtime cap. Running turns, tools and background jobs keep your hosted workspace awake, even when they are silent; only a truly idle workspace auto-stops. Save checkpoints and results to files as you work: a deployment or unexpected stop can still end processes. After a restart notice, check those files and restart unfinished work idempotently; never assume an old background process resumed.
|
|
26
|
+
|
|
27
|
+
In a hosted workspace, every teammate you start keeps its own process in the workspace memory, even while it waits. At most 2 can run at once; a 3rd start is refused. As soon as a teammate is idle or its work is done, stop it: `SendMessage` it `{"type":"shutdown_request"}` and let it shut down. Start a new one when new work needs it.
|
|
28
|
+
|
|
23
29
|
## Always
|
|
24
30
|
|
|
25
31
|
- Answer every message in the channel it came from. A human who wrote is waiting for you; answer them first.
|
|
@@ -26,7 +26,7 @@ Keep each human's page in company memory current, every fact cited, as the Human
|
|
|
26
26
|
|
|
27
27
|
In your first session, call `read_onboarding_obligations` once (reading them acknowledges the safety rules), then `read_readiness`.
|
|
28
28
|
|
|
29
|
-
Your primary objective is to reach your quotas and drive the organisation forward in your role. Report each metric you own, and your subordinates' when needed, with `report_metric`: only true numbers, in good faith, each backed by `evidence` of what you counted it from, or by `fromEventId` when a human told you the number. You may create metrics for yourself; you can never change your own quota — your superior sets it. Talk only in shared spaces (channels and tasks). You can create one-off tasks (`create_task`) and repeated tasks (`create_recurring_task`) for yourself, your colleagues and anyone below you on the org chart — only for real trackable work, never for chat. You also manage your subordinates' tasks and repeated tasks (`update_task`, `update_recurring_task`, `stop_recurring_task`) and their metrics: edit a definition (`edit_metric`) or remove a wrong value with a reason (`void_metric_record`).
|
|
29
|
+
Your primary objective is to reach your quotas and drive the organisation forward in your role. Report each metric you own, and your subordinates' when needed, with `report_metric`: only true numbers, in good faith, each backed by `evidence` of what you counted it from, or by `fromEventId` when a human told you the number. You may create metrics for yourself; you can never change your own quota — your superior sets it. Talk only in shared spaces (channels and tasks). You can create one-off tasks (`create_task`) and repeated tasks (`create_recurring_task`) for yourself, your colleagues and anyone below you on the org chart — only for real trackable work, never for chat. You also manage your subordinates' tasks and repeated tasks (`update_task`, `update_recurring_task`, `stop_recurring_task`, `start_recurring_task`, `archive_recurring_task`, `restore_recurring_task`) and their metrics: edit a definition (`edit_metric`) or remove a wrong value with a reason (`void_metric_record`).
|
|
30
30
|
|
|
31
31
|
## Talking to humans
|
|
32
32
|
|
package/dist/channel.js
CHANGED
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, statSync } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
|
-
import {
|
|
8
|
+
import { awpEndpoint } from "./tool-run.js";
|
|
9
9
|
import { noteCliUpdate } from "./update.js";
|
|
10
10
|
import { trustedMcpEndpoint } from "./setup.js";
|
|
11
11
|
import { syncJoinedSkills } from "./skill-install.js";
|
|
12
12
|
export const CHANNEL_GUIDANCE = "AgentRadar channel messages are wake signals only. Receive the events with `agentradar run $AR_CLI_RUNNER_ID inbox next`; the next call acknowledges the batch you read. Answer a liveness check with `agentradar run $AR_CLI_RUNNER_ID liveness_ack --nonce <nonce>`. Never acknowledge an event you have not read. Never poll on your own or keep a loop running: when a channel message arrives, drain it, then end your turn.";
|
|
13
|
-
export const CHANNEL_BLOCKED = "AgentRadar has not seen this session run `agentradar run $AR_CLI_RUNNER_ID inbox next`. Channel delivery is unconfirmed. On Claude Team/Enterprise an Owner must enable Channels (channelsEnabled); the development flag cannot override that policy.
|
|
13
|
+
export const CHANNEL_BLOCKED = "AgentRadar has not seen this session run `agentradar run $AR_CLI_RUNNER_ID inbox next`. Channel delivery is unconfirmed. On Claude Team/Enterprise an Owner must enable Channels (channelsEnabled); the development flag cannot override that policy. Your company sees this agent as not receiving messages until the channel works.";
|
|
14
|
+
// The runner's frame text (templates/vendor-runner, shipped beside this file as dist/vendor-runner), so both say a restart one way.
|
|
15
|
+
const runnerFrame = () => import(new URL(import.meta.url.endsWith(".ts")
|
|
16
|
+
? "../../../templates/vendor-runner/session-frame.mjs" : "./vendor-runner/session-frame.mjs", import.meta.url).href);
|
|
14
17
|
/** The channel: a local stdio MCP with no tools, only wake signals. The agent acts through the CLI; wake signals never consume the durable queue. */
|
|
18
|
+
/** The wake line. Zero events never says "run inbox next": that call has nothing to return. Each reason names its own tool. */
|
|
19
|
+
export function wakeText(runnerId, count, payload) {
|
|
20
|
+
const run = `agentradar run ${runnerId}`;
|
|
21
|
+
if (count)
|
|
22
|
+
return `${payload.firstStep ? `${payload.firstStep} ` : ""}${count} new AgentRadar events; run \`${run} inbox next\`${payload.livenessProbe ? " to receive your liveness check" : ""}.`;
|
|
23
|
+
return [
|
|
24
|
+
payload.onboarding && `AgentRadar session started, no messages waiting. Run \`${run} get_my_context\` now for your identity, peers and rules.`,
|
|
25
|
+
payload.livenessProbe && `AgentRadar liveness check, no messages waiting. Run \`${run} liveness_ack --nonce ${payload.livenessProbe.nonce}\` now.`,
|
|
26
|
+
].filter(Boolean).join(" ");
|
|
27
|
+
}
|
|
15
28
|
export async function startChannel(runnerId, deps = {}) {
|
|
16
29
|
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(runnerId))
|
|
17
30
|
throw new Error("Invalid runner ID");
|
|
18
31
|
const home = deps.home ?? homedir();
|
|
19
|
-
const config = await readFile(join(home, ".agentradar", "agents", `${runnerId}.env`), "utf8");
|
|
20
|
-
const endpoint = trustedMcpEndpoint(envValue(config, "AWP_MCP_URL") ?? "");
|
|
21
|
-
const bearer = envValue(config, "AWP_BEARER");
|
|
22
|
-
if (!endpoint || !bearer)
|
|
23
|
-
throw new Error("Runner configuration requires a trusted AWP_MCP_URL and AWP_BEARER");
|
|
24
32
|
const output = deps.output ?? process.stdout;
|
|
25
33
|
const logError = deps.logError ?? console.error;
|
|
34
|
+
// The runner file, else a hosted session's own PAPERCLIP_API_* (hosted-session-start.mjs puts them in this server's env).
|
|
35
|
+
const target = await awpEndpoint(runnerId, { home, logError });
|
|
36
|
+
const endpoint = trustedMcpEndpoint(target?.url ?? "");
|
|
37
|
+
const bearer = target?.bearer;
|
|
38
|
+
if (!endpoint || !bearer)
|
|
39
|
+
throw new Error("Runner configuration requires a trusted AWP_MCP_URL and AWP_BEARER");
|
|
40
|
+
// TRC-RESTART-REASON: the hosted bridge leaves why the last workspace stopped; the first wake with work says it, once.
|
|
41
|
+
const notice = join(home, ".agentradar", "run", "restart-notice.json");
|
|
26
42
|
const sessionId = randomUUID();
|
|
27
43
|
const stop = new AbortController();
|
|
28
44
|
let sequence = 0;
|
|
@@ -55,7 +71,7 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
55
71
|
});
|
|
56
72
|
await noteCliUpdate(response.headers, logError);
|
|
57
73
|
if (!response.ok) {
|
|
58
|
-
const said =
|
|
74
|
+
const said = await response.json().then((body) => body.error?.message).catch(() => null);
|
|
59
75
|
throw new Error(said ?? `AgentRadar refused the channel call (${response.status})`);
|
|
60
76
|
}
|
|
61
77
|
const text = await response.text();
|
|
@@ -65,9 +81,14 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
65
81
|
return JSON.parse(json);
|
|
66
82
|
};
|
|
67
83
|
const next = (args = {}) => remote("tools/call", { name: "next_event", arguments: { ...args, sessionId } });
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
// AGENT-GUARD-ACTIVE: the supervisor's marker, written by agent-guard's SessionStart hook in this session.
|
|
85
|
+
const guardLoaded = () => !process.env.AR_GUARD_MARKER || existsSync(process.env.AR_GUARD_MARKER);
|
|
86
|
+
const pause = (until) => new Promise((resolve) => {
|
|
87
|
+
const done = () => { clearTimeout(timer); clearInterval(watch); stop.signal.removeEventListener("abort", done); resolve(); };
|
|
70
88
|
const timer = setTimeout(done, deps.retryMs ?? 3_000);
|
|
89
|
+
// COLD-START-WARMUP: a booting session's work is held until it polls ready, so poll the moment the marker lands.
|
|
90
|
+
const watch = until && setInterval(() => { if (until())
|
|
91
|
+
done(); }, 100);
|
|
71
92
|
stop.signal.addEventListener("abort", done, { once: true });
|
|
72
93
|
if (stop.signal.aborted)
|
|
73
94
|
done();
|
|
@@ -76,8 +97,10 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
76
97
|
let lastWake = "";
|
|
77
98
|
let lastSent = 0;
|
|
78
99
|
let selftestStarted = false;
|
|
100
|
+
let guarded = true;
|
|
79
101
|
while (!stop.signal.aborted) {
|
|
80
102
|
try {
|
|
103
|
+
guarded = guardLoaded(); // first, so a call that throws after the marker lands still gets the full pause
|
|
81
104
|
if (!selftestStarted) {
|
|
82
105
|
// Establish the heartbeat without entering the empty queue's 200-second hold first.
|
|
83
106
|
const connected = await next({ runnerState: "starting" });
|
|
@@ -95,8 +118,6 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
95
118
|
if (unconfirmed === "reported" && polled())
|
|
96
119
|
unconfirmed = null;
|
|
97
120
|
// While reported, no runnerState: the stored state stands, and the poll still drains for wake signals.
|
|
98
|
-
// AGENT-GUARD-ACTIVE: the supervisor's marker, written by agent-guard's SessionStart hook in this session.
|
|
99
|
-
const guarded = !process.env.AR_GUARD_MARKER || existsSync(process.env.AR_GUARD_MARKER);
|
|
100
121
|
const response = await next(unconfirmed ? {} : { runnerState: guarded ? "ready" : "starting" });
|
|
101
122
|
if (response.error || response.result?.isError)
|
|
102
123
|
throw new Error("AgentRadar refused next_event; check the runner key and its poll permission");
|
|
@@ -105,10 +126,13 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
105
126
|
const events = payload.events ?? [];
|
|
106
127
|
const wake = JSON.stringify([events.map((event) => event.eventId), payload.livenessProbe?.nonce, Boolean(payload.onboarding)]);
|
|
107
128
|
if ((events.length || payload.livenessProbe || payload.onboarding) && (wake !== lastWake || Date.now() - lastSent >= 30_000)) {
|
|
129
|
+
const restart = events.length ? (await runnerFrame()).restartLine(await readFile(notice, "utf8").then(JSON.parse, () => null), runnerId) : "";
|
|
108
130
|
await send({ jsonrpc: "2.0", method: "notifications/claude/channel", params: {
|
|
109
|
-
content: `${
|
|
131
|
+
content: `${restart ? `${restart} ` : ""}${wakeText(runnerId, events.length, payload)}`,
|
|
110
132
|
meta: { count: String(events.length) },
|
|
111
133
|
} });
|
|
134
|
+
if (restart)
|
|
135
|
+
await rm(notice, { force: true });
|
|
112
136
|
lastWake = wake;
|
|
113
137
|
lastSent = Date.now();
|
|
114
138
|
}
|
|
@@ -120,7 +144,7 @@ export async function startChannel(runnerId, deps = {}) {
|
|
|
120
144
|
if (!stop.signal.aborted)
|
|
121
145
|
logError(/below the minimum/.test(said) ? said : "AgentRadar channel poll failed; retrying. Check connectivity and the runner key.");
|
|
122
146
|
}
|
|
123
|
-
await pause();
|
|
147
|
+
await pause(guarded ? undefined : guardLoaded);
|
|
124
148
|
}
|
|
125
149
|
};
|
|
126
150
|
const handle = async (message) => {
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { installVendorSkill } from "./skill-install.js";
|
|
|
5
5
|
import { startChannel } from "./channel.js";
|
|
6
6
|
import { runRestartSelftest, startVendorRunner } from "./runner.js";
|
|
7
7
|
import { cancelJoin, CODEX_GUARD_REFUSAL, dropStaleMcp, ensureCodexGuard, joinOrganisation, leaveOrganisation, setupVendorAgent } from "./setup.js";
|
|
8
|
-
import { callAwpTool, callToolOverCli,
|
|
8
|
+
import { callAwpTool, callToolOverCli, EXIT, parseCallArgs } from "./tool-run.js";
|
|
9
9
|
import { installSyncTimer, syncAgentDir } from "./sync.js";
|
|
10
10
|
import { importVaultOverCli } from "./vault-import.js";
|
|
11
11
|
import { cloneVaultOverCli } from "./vault-clone.js";
|
|
@@ -17,25 +17,41 @@ export const runArgs = (raw) => raw[0] !== "run" ? raw
|
|
|
17
17
|
: raw[1] === "join" ? raw.slice(1)
|
|
18
18
|
: /^https?:\/\//.test(raw[1] ?? "") ? ["join", ...raw.slice(1)]
|
|
19
19
|
: ["call", ...raw.slice(1)];
|
|
20
|
+
const USAGE = [
|
|
21
|
+
"Usage: agentradar <noun> [<sub-noun>] <verb> [--<flag> <value> | --<flag>-file <path> ...] [--dry-run] [--json] [--runner <id>]",
|
|
22
|
+
" Every agent command comes from the server. agentradar --help lists the groups (as $AR_CLI_RUNNER_ID),",
|
|
23
|
+
" agentradar <group> --help a group's commands, agentradar <command> --help its flags.",
|
|
24
|
+
" Examples:",
|
|
25
|
+
" agentradar inbox next",
|
|
26
|
+
" agentradar channels reply --thread-id <id> --text \"On it\"",
|
|
27
|
+
" agentradar tasks create --title \"Draft the brief\" --dry-run",
|
|
28
|
+
" The runner is $AR_CLI_RUNNER_ID, or --runner <id>. --dry-run prints the exact call and sends nothing.",
|
|
29
|
+
" Exit codes: 0 done; 1 refused or unreachable (the reason is on stderr); 2 the command was wrong.",
|
|
30
|
+
"Local commands:",
|
|
31
|
+
" agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--no-service | --service]",
|
|
32
|
+
" agentradar join cancel <join-link|request-id>",
|
|
33
|
+
" agentradar setup [--trust] [--harness claude|codex]",
|
|
34
|
+
" agentradar leave <runner-id>",
|
|
35
|
+
" agentradar channel <runner-id>",
|
|
36
|
+
" agentradar selftest <runner-id> [--status|--restart]",
|
|
37
|
+
" agentradar runner start <runner-id>",
|
|
38
|
+
" agentradar call <runner-id> <tool | command> [--<flag> <value> ...] [--args '<json>']",
|
|
39
|
+
" agentradar vault import <runner-id> <dir> [--vault <vault-id>] [--source <name>] [--write]",
|
|
40
|
+
" agentradar vault clone <runner-id> <vault-id> [dir]",
|
|
41
|
+
" agentradar secrets pull|renew|env <runner-id> | agentradar secrets exec <runner-id> -- <command>",
|
|
42
|
+
" agentradar secrets upload|rotate <runner-id> --env-file <path> | --from-env <NAME,...> | --prompt <NAME> [--replace]",
|
|
43
|
+
" agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]",
|
|
44
|
+
" agentradar admin broadcast --dry-run|--send --target all --text <text> --cta <next step>",
|
|
45
|
+
" agentradar upgrade | agentradar --version",
|
|
46
|
+
].join("\n");
|
|
20
47
|
async function main() {
|
|
21
48
|
const args = runArgs(process.argv.slice(2));
|
|
22
49
|
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
23
|
-
console.log(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
" agentradar run <runner-id> thread --help a group's verbs",
|
|
29
|
-
" agentradar run <runner-id> thread reply --help one command's arguments",
|
|
30
|
-
" Examples:",
|
|
31
|
-
" agentradar run r1 inbox next",
|
|
32
|
-
" agentradar run r1 thread reply --thread-id <id> --text \"On it\"",
|
|
33
|
-
" agentradar run r1 inbox ack --message-ids <id> --message-ids <id>",
|
|
34
|
-
" agentradar run r1 task create --title \"Draft the brief\" --dry-run",
|
|
35
|
-
" --dry-run prints the exact call and sends nothing. Output is JSON. Exit codes: 0 done; 1 refused or",
|
|
36
|
-
" unreachable (reason on stderr); 2 the command was wrong.",
|
|
37
|
-
"agentradar upgrade install the latest agentradar-cli",
|
|
38
|
-
].join("\n"));
|
|
50
|
+
console.log(USAGE);
|
|
51
|
+
// The agent's own groups, from its server, when this shell is a runner's.
|
|
52
|
+
const runner = process.env.AR_CLI_RUNNER_ID;
|
|
53
|
+
if (runner)
|
|
54
|
+
process.exitCode = await callToolOverCli(runner, "", undefined, {}, [["help", ""]]);
|
|
39
55
|
return;
|
|
40
56
|
}
|
|
41
57
|
if (args.length === 1 && args[0] === "upgrade") {
|
|
@@ -70,7 +86,7 @@ async function main() {
|
|
|
70
86
|
await startChannel(args[1]);
|
|
71
87
|
return;
|
|
72
88
|
}
|
|
73
|
-
if (args[0] === "selftest" && args.length === 3 && args[2] === "--restart") {
|
|
89
|
+
if (args[0] === "selftest" && args[1] !== "run" && args.length === 3 && args[2] === "--restart") {
|
|
74
90
|
// A native-channel session resumes in this terminal; print the result after it closes, not over its screen.
|
|
75
91
|
const report = [];
|
|
76
92
|
process.once("exit", () => { if (report.length)
|
|
@@ -78,7 +94,7 @@ async function main() {
|
|
|
78
94
|
await runRestartSelftest(args[1], { log: (text) => report.push(text) });
|
|
79
95
|
return;
|
|
80
96
|
}
|
|
81
|
-
if (args[0] === "selftest" && (args.length === 2 || (args.length === 3 && args[2] === "--status"))) {
|
|
97
|
+
if (args[0] === "selftest" && args[1] !== "run" && (args.length === 2 || (args.length === 3 && args[2] === "--status"))) {
|
|
82
98
|
process.exitCode = await callAwpTool(args[1], "run_selftest", { statusOnly: args[2] === "--status" }, {});
|
|
83
99
|
return;
|
|
84
100
|
}
|
|
@@ -110,18 +126,18 @@ async function main() {
|
|
|
110
126
|
for (let i = 1; i < args.length; i++) {
|
|
111
127
|
if (["--name", "--model", "--attach-tmux", "--session"].includes(args[i]))
|
|
112
128
|
named[args[i].slice(2)] = args[++i] ?? "-";
|
|
113
|
-
else if (!["--trust", "--attach-current", "--allow-bypass", "--revoke-bypass", "--feeder"].includes(args[i]))
|
|
129
|
+
else if (!["--trust", "--attach-current", "--allow-bypass", "--revoke-bypass", "--feeder", "--service", "--no-service"].includes(args[i]))
|
|
114
130
|
urls.push(args[i]);
|
|
115
131
|
}
|
|
116
|
-
if (urls.length !== 1 || [urls[0], ...Object.values(named)].some((value) => value.startsWith("-")) || (named["attach-tmux"] && args.includes("--attach-current"))) {
|
|
117
|
-
console.error("Usage: agentradar join <join-link> [--name <your name>] [--model <your model id>] [--trust] [--attach-tmux <session[:window.pane]> | --attach-current] [--session <claude session id>] [--revoke-bypass] [--
|
|
132
|
+
if (urls.length !== 1 || [urls[0], ...Object.values(named)].some((value) => value.startsWith("-")) || (named["attach-tmux"] && args.includes("--attach-current")) || (args.includes("--service") && args.includes("--no-service"))) {
|
|
133
|
+
console.error("Usage: agentradar join <join-link> [--name <your name>] [--model <your model id>] [--trust] [--attach-tmux <session[:window.pane]> | --attach-current (Codex and other CLIs)] [--session <claude session id>] [--revoke-bypass] [--no-service | --service]");
|
|
118
134
|
process.exitCode = 1;
|
|
119
135
|
return;
|
|
120
136
|
}
|
|
121
137
|
process.exitCode = await joinOrganisation(urls[0], {
|
|
122
138
|
trust: args.includes("--trust"), agentName: named.name, model: named.model, session: named.session,
|
|
123
139
|
attach: args.includes("--attach-current") ? "current" : named["attach-tmux"],
|
|
124
|
-
revokeBypass: args.includes("--revoke-bypass"), feeder: args.includes("--feeder"),
|
|
140
|
+
revokeBypass: args.includes("--revoke-bypass"), feeder: args.includes("--feeder"), service: args.includes("--service"),
|
|
125
141
|
});
|
|
126
142
|
return;
|
|
127
143
|
}
|
|
@@ -134,21 +150,16 @@ async function main() {
|
|
|
134
150
|
process.exitCode = await startVendorRunner(args[2]);
|
|
135
151
|
return;
|
|
136
152
|
}
|
|
137
|
-
if (args
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!parsed || (!call && parsed.flags.length)) {
|
|
145
|
-
console.error(call ? "Usage: agentradar run <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar run <runner-id> [<tool>] --help" : "Usage: agentradar tool run <runner-id> <tool-key> [--args '<json>']");
|
|
146
|
-
process.exitCode = 1;
|
|
153
|
+
if (args[0] === "call") {
|
|
154
|
+
// `call <runner-id> <tool | command>`: the runner named in place, for a shell that has no AR_CLI_RUNNER_ID.
|
|
155
|
+
const parsed = parseCallArgs(args.slice(1));
|
|
156
|
+
const [runnerId, ...words] = parsed?.words ?? [];
|
|
157
|
+
if (!parsed || !runnerId || words.length > 3 || (!words.length && !parsed.flags.some(([flag]) => flag === "help"))) {
|
|
158
|
+
console.error("Usage: agentradar call <runner-id> <tool | command> [--<flag> <value> ...] [--args '<json>'] | agentradar call <runner-id> [<command>] --help");
|
|
159
|
+
process.exitCode = EXIT.usage;
|
|
147
160
|
return;
|
|
148
161
|
}
|
|
149
|
-
process.exitCode =
|
|
150
|
-
? await callToolOverCli(parsed.runnerId, parsed.tool, parsed.json, {}, parsed.flags)
|
|
151
|
-
: await runServiceToolOverCli(parsed.runnerId, parsed.tool, parsed.json);
|
|
162
|
+
process.exitCode = await callToolOverCli(runnerId, words.join(" "), parsed.json, {}, parsed.flags);
|
|
152
163
|
return;
|
|
153
164
|
}
|
|
154
165
|
if (args[0] === "vault" && args[1] === "import") {
|
|
@@ -171,61 +182,8 @@ async function main() {
|
|
|
171
182
|
process.exitCode = await cloneVaultOverCli(args[2], args[3], args[4]);
|
|
172
183
|
return;
|
|
173
184
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (!parsed || parsed.tool !== "inbox_search") {
|
|
177
|
-
console.error(INBOX_SEARCH_USAGE);
|
|
178
|
-
process.exitCode = 1;
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
process.exitCode = await inboxSearchOverCli(parsed.runnerId, parsed.flags, args.includes("--json"));
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (args[0] === "metric" && args[1] === "report") {
|
|
185
|
-
// report_metric over the same endpoint and key: an agent's own metric, a subordinate's, or a person's below it.
|
|
186
|
-
const parsed = parseCallArgs([...args.slice(2), "report_metric"]);
|
|
187
|
-
if (!parsed || !parsed.flags.some(([flag]) => flag === "metric-id")) {
|
|
188
|
-
console.error(METRIC_REPORT_USAGE);
|
|
189
|
-
process.exitCode = 1;
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
process.exitCode = await callToolOverCli(parsed.runnerId, parsed.tool, undefined, {}, parsed.flags);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (args[0] === "propose-human") {
|
|
196
|
-
// R013: the Chief invites a human it found into a vacant role, the MCP `propose_human` over the same endpoint and key.
|
|
197
|
-
const parsed = parseCallArgs([...args.slice(1), "propose_human"]);
|
|
198
|
-
if (!parsed || !["name", "email", "role-id"].every((k) => parsed.flags.some(([flag]) => flag === k))) {
|
|
199
|
-
console.error(PROPOSE_HUMAN_USAGE);
|
|
200
|
-
process.exitCode = 1;
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
process.exitCode = await callToolOverCli(parsed.runnerId, parsed.tool, undefined, {}, parsed.flags);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
if (args[0] === "listing" && args[1] === "describe") {
|
|
207
|
-
// UAT U1: the agent's public catalog description, the MCP `set_listing_description` over the same endpoint and key.
|
|
208
|
-
const parsed = parseCallArgs([...args.slice(2), "set_listing_description"]);
|
|
209
|
-
if (!parsed || !parsed.flags.some(([flag]) => flag === "capabilities")) {
|
|
210
|
-
console.error(LISTING_DESCRIBE_USAGE);
|
|
211
|
-
process.exitCode = 1;
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
process.exitCode = await callToolOverCli(parsed.runnerId, parsed.tool, undefined, {}, parsed.flags);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (args[0] === "permissions" && args[1] && Object.hasOwn(PERMISSION_VERB_TOOLS, args[1])) {
|
|
218
|
-
const tool = PERMISSION_VERB_TOOLS[args[1]];
|
|
219
|
-
const parsed = parseCallArgs([...args.slice(2), tool]);
|
|
220
|
-
if (!parsed || parsed.tool !== tool) {
|
|
221
|
-
console.error("Usage: agentradar permissions mine|roles|set|request|grant-tool <runner-id> [--<argument> <value> ...]");
|
|
222
|
-
process.exitCode = 1;
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
process.exitCode = await callToolOverCli(parsed.runnerId, tool, parsed.json, {}, parsed.flags);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (args[0] === "secrets") {
|
|
185
|
+
// The local secrets verbs take the runner in place; `secrets list|upload|grant|revoke|rotate --<flag>` are the server's.
|
|
186
|
+
if (args[0] === "secrets" && ["pull", "renew", "env", "exec", "upload", "rotate"].includes(args[1] ?? "") && args[2] && !args[2].startsWith("-")) {
|
|
229
187
|
process.exitCode = await secretsOverCli(args.slice(1));
|
|
230
188
|
return;
|
|
231
189
|
}
|
|
@@ -233,15 +191,6 @@ async function main() {
|
|
|
233
191
|
process.exitCode = await adminOverCli(args.slice(1));
|
|
234
192
|
return;
|
|
235
193
|
}
|
|
236
|
-
if (args[0] === "task" && args[1] && Object.hasOwn(TASK_VERB_TOOLS, args[1])) {
|
|
237
|
-
if (args.length !== 5 || args[3] !== "--args") {
|
|
238
|
-
console.error("Usage: agentradar task create|repeat|make-recurring <runner-id> --args '<json>'");
|
|
239
|
-
process.exitCode = 1;
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
process.exitCode = await createTaskOverCli(args[2], args[1], args[4]);
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
194
|
if (args.length === 2 && args[0] === "skill" && args[1] === "install") {
|
|
246
195
|
await installVendorSkill();
|
|
247
196
|
return;
|
|
@@ -264,10 +213,17 @@ async function main() {
|
|
|
264
213
|
process.exitCode = await syncAgentDir(args[1]);
|
|
265
214
|
return;
|
|
266
215
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
216
|
+
// #1871: every other command is the server's, `<noun> [<sub-noun>] <verb>`, run as this shell's runner.
|
|
217
|
+
const parsed = parseCallArgs(args);
|
|
218
|
+
const runner = parsed?.flags.find(([flag]) => flag === "runner")?.[1] ?? process.env.AR_CLI_RUNNER_ID;
|
|
219
|
+
if (!parsed || !runner || parsed.words.length > 3 || (!parsed.words.length && !parsed.flags.some(([flag]) => flag === "help"))) {
|
|
220
|
+
console.error(USAGE);
|
|
221
|
+
if (parsed && !runner)
|
|
222
|
+
console.error("Agent commands run as a runner: set AR_CLI_RUNNER_ID or pass --runner <id>.");
|
|
223
|
+
process.exitCode = EXIT.usage;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
process.exitCode = await callToolOverCli(runner, parsed.words.join(" "), parsed.json, {}, parsed.flags.filter(([flag]) => flag !== "runner"));
|
|
271
227
|
}
|
|
272
228
|
/**
|
|
273
229
|
* 🔴 `error.message` ALONE IS TWO WORDS ON THE FAILURE A VENDOR IS MOST LIKELY TO HIT. Node's `fetch`
|
package/dist/setup.js
CHANGED
|
@@ -469,13 +469,17 @@ async function installRunner(home, projectPath, approved, service, feed) {
|
|
|
469
469
|
...(service.env.AGENTRADAR_INPUT_READY_PATTERN ? [`AR_INPUT_READY_PATTERN=${shellValue(service.env.AGENTRADAR_INPUT_READY_PATTERN)}`] : []), `AGENT_PID_PATTERN=${shellValue(basename(bin).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))}`);
|
|
470
470
|
}
|
|
471
471
|
else {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
joined.push(`
|
|
472
|
+
if (service.inPlace)
|
|
473
|
+
joined.push("AR_INPLACE=1"); // the user's own launcher runs claude; no supervisor reads this file
|
|
474
|
+
else {
|
|
475
|
+
// The caller's own conversation, both times: a resume that fails tries it once more, never a fresh session.
|
|
476
|
+
joined.push(`AR_RESUME_FLAGS=${shellValue(`--resume ${service.sessionId}`)}`, `AR_FRESH_FLAGS=${shellValue(`--resume ${service.sessionId}`)}`);
|
|
477
|
+
// A person's model switch: the feeder and agent-guard's hooks meet in this directory, keyed by the session.
|
|
478
|
+
joined.push(`AR_CLAUDE_SESSION_ID=${shellValue(service.sessionId)}`, `AR_MODEL_SWITCH_DIR=${shellValue(join(configDir, "model-switch"))}`);
|
|
479
|
+
const configDirEnv = service.env.CLAUDE_CONFIG_DIR?.trim();
|
|
480
|
+
if (configDirEnv)
|
|
481
|
+
joined.push(`CLAUDE_CONFIG_DIR=${shellValue(resolve(configDirEnv))}`);
|
|
482
|
+
}
|
|
479
483
|
// agent-guard, enabled for this session only (`--settings`), loaded from the copy inside this package, with
|
|
480
484
|
// its plugin state under the runner's config so the vendor's own Claude plugins are never touched.
|
|
481
485
|
const settings = join(configDir, "agentradar-settings.json");
|
|
@@ -487,16 +491,21 @@ async function installRunner(home, projectPath, approved, service, feed) {
|
|
|
487
491
|
// Nobody sits at the pane to accept bypass mode's warning screen; the operator's consent was given at approval.
|
|
488
492
|
...(service.bypass ? { skipDangerousModePermissionPrompt: true } : {}),
|
|
489
493
|
}, null, 2)}\n`);
|
|
490
|
-
if (service.
|
|
491
|
-
|
|
492
|
-
|
|
494
|
+
if (!service.inPlace) {
|
|
495
|
+
if (service.bypass)
|
|
496
|
+
joined.push(`CLAUDE_EXTRA_FLAGS=${shellValue("--permission-mode bypassPermissions")}`);
|
|
497
|
+
joined.push(`CLAUDE_SETTINGS=${shellValue(settings)}`, `CLAUDE_CODE_PLUGIN_CACHE_DIR=${shellValue(join(configDir, "plugins"))}`);
|
|
498
|
+
// Vadym 13444: the agent keeps every MCP and channel it already has; AgentRadar's channel is added next to them.
|
|
499
|
+
joined.push("AR_MCP_STRICT=0");
|
|
500
|
+
}
|
|
493
501
|
}
|
|
494
502
|
if (service.attach) {
|
|
495
503
|
// The caller's own tmux pane: the feeder types into it; the supervisor never starts, kills or streams it.
|
|
496
504
|
// TMUX names the caller's tmux server, which a service started by launchd or systemd would not find.
|
|
497
505
|
joined.push("AR_ATTACH=1", `TMUX_SESSION=${shellValue(service.attach)}`, "TERMINAL_BRIDGE_ENABLED=0", ...(service.env.TMUX ? [`TMUX=${shellValue(service.env.TMUX)}`] : []), ...(harness === "generic" ? [] : [`AGENT_PID_PATTERN=${shellValue(`(^|/)${harness ?? "claude"}( |$)`)}`]));
|
|
498
506
|
}
|
|
499
|
-
|
|
507
|
+
// An in-place join has no supervisor to read tool.env, so the caller's gateway credentials are not copied to disk.
|
|
508
|
+
const toolEnv = service.inPlace ? [] : Object.entries(service.env)
|
|
500
509
|
.filter(([key, value]) => (TOOL_ENV.test(key) || toolKeys.includes(key)) && key !== "ANTHROPIC_API_KEY" && value && !value.includes("\n"))
|
|
501
510
|
.map(([key, value]) => `${key}=${value}`);
|
|
502
511
|
if (toolEnv.length) {
|
|
@@ -614,6 +623,7 @@ export async function setupVendorAgent(deps = {}) {
|
|
|
614
623
|
throw new Error("AgentRadar setup code expired; run setup again");
|
|
615
624
|
}
|
|
616
625
|
export const bypassOnNotice = (link) => `Bypass-permissions mode is ON on this machine: the agent runs without permission prompts. To turn it off: agentradar join ${link} --revoke-bypass`;
|
|
626
|
+
export const CLAUDE_CHANNEL_ONLY = "A Claude Code agent receives work only through the AgentRadar channel. --feeder, AGENTRADAR_FEED=feeder, --attach-tmux and --attach-current are for Codex and other CLIs; run join again without them.";
|
|
617
627
|
export const BYPASS_REVOKED = "Bypass-permissions mode is off on this machine now; the agent asks before acting again.";
|
|
618
628
|
/** A sortable key for `x.y.z[-pre]`; a release sorts above its prereleases. ponytail: prereleases of one version tie. */
|
|
619
629
|
const versionRank = (version) => {
|
|
@@ -720,36 +730,34 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
720
730
|
}
|
|
721
731
|
}
|
|
722
732
|
const label = harness === "generic" ? basename((env.AGENTRADAR_AGENT_BIN ?? "").trim()) : harness === "claude" ? "Claude Code" : "Codex";
|
|
723
|
-
// Vadym 10342:
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
|
|
733
|
+
// Vadym 10342 + 13430: a Claude Code agent connects through the AgentRadar channel only; nothing is ever typed
|
|
734
|
+
// into its input. The tmux feeder is for the CLIs that have no channel (Codex, generic).
|
|
735
|
+
const channel = harness === "claude";
|
|
736
|
+
// Vadym 13444: a Claude Code agent joins in place. Nothing is started, resumed or replaced; the user's own launcher
|
|
737
|
+
// gets two files and the flags to add (channel-only-claude.md §2). `--service` keeps the AgentRadar-run service.
|
|
738
|
+
const inPlace = channel && !deps.service;
|
|
739
|
+
if (channel && (deps.feeder || env.AGENTRADAR_FEED?.trim().toLowerCase() === "feeder" || deps.attach))
|
|
740
|
+
throw new Error(CLAUDE_CHANNEL_ONLY);
|
|
727
741
|
// The agent is a persistent session the service owns, not this one: no MCP entry in this folder (it acts
|
|
728
742
|
// through `agentradar run`), a runner env pinned to its own session, and a LaunchAgent / systemd unit.
|
|
729
743
|
// Which session the agent is, settled before any request: a key is issued once, so never claim one for a session that cannot run.
|
|
730
744
|
const settle = async () => {
|
|
731
745
|
const tmux = deps.tmux ?? (async (args) => (await promisify(execFile)(env.TMUX_BIN || "tmux", args, { env })).stdout);
|
|
732
|
-
// Attach to the caller
|
|
733
|
-
// pane. The service's own session (join-<request>) re-running join is not a caller to attach to.
|
|
746
|
+
// Attach to the caller's tmux pane when asked for by flag (Codex and generic only; Claude Code is refused above).
|
|
734
747
|
let attach = deps.attach === "current" ? env.TMUX_PANE : deps.attach;
|
|
735
748
|
if (deps.attach === "current" && !attach)
|
|
736
749
|
throw new Error("--attach-current needs a session running inside tmux");
|
|
737
|
-
// Never in channel mode: nothing types into the caller's pane.
|
|
738
|
-
if (!channel && !deps.attach && harness === "claude" && env.CLAUDECODE && env.TMUX && env.TMUX_PANE)
|
|
739
|
-
attach = env.TMUX_PANE;
|
|
740
750
|
if (attach) {
|
|
741
751
|
// A pane id (%3) dies with the tmux server; session:window.pane comes back when its session is recreated.
|
|
742
752
|
attach = (await tmux(["display-message", "-p", "-t", attach, "#{session_name}:#{window_index}.#{pane_index}"])).trim();
|
|
743
753
|
if (!attach)
|
|
744
754
|
throw new Error("tmux found no such pane");
|
|
745
|
-
if (!deps.attach && attach.startsWith("join-"))
|
|
746
|
-
attach = undefined;
|
|
747
755
|
}
|
|
748
756
|
// Unattended, the service resumes the caller's own Claude conversation, never a stranger's. An attached
|
|
749
757
|
// pane is never started from here, so there its session id only keys the model switch.
|
|
750
|
-
const sessionId = harness !== "claude" ? undefined
|
|
751
|
-
: await callerSession(deps, env, home, await realpath(cwd)).catch((error) => { if (
|
|
752
|
-
|
|
758
|
+
const sessionId = harness !== "claude" || inPlace ? undefined
|
|
759
|
+
: await callerSession(deps, env, home, await realpath(cwd)).catch((error) => { if (deps.session)
|
|
760
|
+
throw error; return undefined; });
|
|
753
761
|
return { attach, sessionId };
|
|
754
762
|
};
|
|
755
763
|
let where;
|
|
@@ -758,14 +766,14 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
758
766
|
* 10-04, after the Codex run stopped on an approval prompt nobody could answer); `--revoke-bypass` takes it
|
|
759
767
|
* back on this machine. An attached pane keeps the permission mode it already runs in.
|
|
760
768
|
*/
|
|
761
|
-
const localBypass = (approvedBypass, revoked) => approvedBypass && !revoked && !where.attach;
|
|
769
|
+
const localBypass = (approvedBypass, revoked) => approvedBypass && !revoked && !where.attach && !inPlace;
|
|
762
770
|
const startJoined = async (requestId, agentId, installation, approvedBypass, bypass, saved) => {
|
|
763
771
|
const runnerId = `join-${requestId}`;
|
|
764
772
|
const { attach } = where;
|
|
765
773
|
const projectPath = harness === "claude"
|
|
766
774
|
? await installProjectMcp(claudeConfigPath(home, env), cwd, null, deps.trust === true)
|
|
767
775
|
: await realpath(cwd);
|
|
768
|
-
const runnerEnv = await installRunner(home, projectPath, { status: "approved", agentId, runnerId, installation, harness }, { sessionId: where.sessionId ?? requestId, env, attach, bypass }, channel ? "push" : undefined);
|
|
776
|
+
const runnerEnv = await installRunner(home, projectPath, { status: "approved", agentId, runnerId, installation, harness }, { sessionId: where.sessionId ?? requestId, env, attach, bypass, inPlace }, channel ? "push" : undefined);
|
|
769
777
|
await saved?.();
|
|
770
778
|
log(`Runner installed → ${runnerEnv}`);
|
|
771
779
|
if (bypass)
|
|
@@ -774,6 +782,15 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
774
782
|
await syncJoinedSkills(runnerId, { home, fetch: fetchImpl, log }).catch((error) => {
|
|
775
783
|
log(`Your AgentRadar skills are not installed yet (${String(error?.message ?? error).split("\n")[0]}); the runner installs them once it is running.`);
|
|
776
784
|
});
|
|
785
|
+
if (inPlace) {
|
|
786
|
+
const config = join(home, ".agentradar", "config", runnerId);
|
|
787
|
+
if (approvedBypass)
|
|
788
|
+
log("Your company allowed bypass-permissions mode; this session keeps the permission mode it already runs in.");
|
|
789
|
+
log(`Connected, nothing started: your session, its MCPs and its channels are untouched. To receive client messages, restart your agent ONCE with these flags added to its own launch command (keep what it already has, including --continue):
|
|
790
|
+
--mcp-config ${join(config, "awp-only.json")} --dangerously-load-development-channels server:agentradar --settings ${join(config, "agentradar-settings.json")}
|
|
791
|
+
and AR_CLI_RUNNER_ID=${runnerId} in its environment. Accept the one-time development-channel prompt when it appears. To stop: agentradar leave ${runnerId}, then drop those flags.`);
|
|
792
|
+
return 0;
|
|
793
|
+
}
|
|
777
794
|
try {
|
|
778
795
|
await (deps.installService ?? ((id) => installAgentService(id, home, env)))(runnerId);
|
|
779
796
|
}
|
|
@@ -791,14 +808,10 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
791
808
|
const mode = bypass ? ", in bypass-permissions mode as your company allowed" : "";
|
|
792
809
|
if (channel) {
|
|
793
810
|
log(`Connected through the AgentRadar channel. This conversation now continues as a service in ${projectPath} (tmux session ${runnerId}${mode}): client messages are pushed into it and it answers there, with no restart from you. Exit this session now (/exit) so only the service carries on; watch it with: tmux attach -t ${runnerId}. To stop it: agentradar leave ${runnerId}`);
|
|
794
|
-
log(`If your company's Claude plan blocks channels, deliver with the feeder instead: agentradar join ${enrolmentUrl} --feeder`);
|
|
795
811
|
log("Running as a service; it restarts on crash and reboot.");
|
|
796
812
|
return 0;
|
|
797
813
|
}
|
|
798
|
-
|
|
799
|
-
log(harness === "claude"
|
|
800
|
-
? `Connected. This conversation now continues as a service in ${projectPath} (tmux session ${runnerId}${mode}): your welcome and every client message are typed into it, and it answers there. Exit this session now (/exit) so only the service carries on; watch it with: tmux attach -t ${runnerId}. To stop it: agentradar leave ${runnerId}`
|
|
801
|
-
: `Connected. Your agent now runs in its own ${label} session in ${projectPath} (tmux session ${runnerId}${mode}): your welcome and every client message are typed into it, and it answers there. There is nothing more to do in this session. To stop it: agentradar leave ${runnerId}`);
|
|
814
|
+
log(`Connected. Your agent now runs in its own ${label} session in ${projectPath} (tmux session ${runnerId}${mode}): your welcome and every client message are typed into it, and it answers there. There is nothing more to do in this session. To stop it: agentradar leave ${runnerId}`);
|
|
802
815
|
log("Running as a service; it restarts on crash and reboot.");
|
|
803
816
|
return 0;
|
|
804
817
|
};
|
|
@@ -990,8 +1003,8 @@ export async function leaveOrganisation(runnerId, deps = {}) {
|
|
|
990
1003
|
await execute("systemctl", ["--user", "disable", "--now", `agentradar-agent@${runnerId}`]).catch(() => undefined);
|
|
991
1004
|
}
|
|
992
1005
|
// The service stopping leaves the session running; `=` matches the tmux session name exactly.
|
|
993
|
-
// An attached pane is the caller's own session: it stays.
|
|
994
|
-
if (envValue(saved, "AR_ATTACH") !== "1")
|
|
1006
|
+
// An attached pane or an in-place join is the caller's own session: it stays.
|
|
1007
|
+
if (envValue(saved, "AR_ATTACH") !== "1" && envValue(saved, "AR_INPLACE") !== "1")
|
|
995
1008
|
await execute(envValue(saved, "TMUX_BIN") ?? "tmux", ["kill-session", "-t", `=${envValue(saved, "TMUX_SESSION") ?? runnerId}`]).catch(() => undefined);
|
|
996
1009
|
for (const path of [envPath, join(home, ".agentradar", "agents", `${runnerId}.delivered.json`)])
|
|
997
1010
|
await rm(path, { force: true });
|