agent-relay-server 0.32.3 → 0.33.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/package.json +2 -2
- package/public/assets/{activity-DT1JGHnp.js → activity-B0_uE6Yh.js} +2 -2
- package/public/assets/{activity-DT1JGHnp.js.map → activity-B0_uE6Yh.js.map} +1 -1
- package/public/assets/{agent-profiles-CrMemMkZ.js → agent-profiles-Rwxrcf9F.js} +2 -2
- package/public/assets/{agent-profiles-CrMemMkZ.js.map → agent-profiles-Rwxrcf9F.js.map} +1 -1
- package/public/assets/{agents-Bl-rrgOy.js → agents-Dp1EXJc8.js} +2 -2
- package/public/assets/{agents-Bl-rrgOy.js.map → agents-Dp1EXJc8.js.map} +1 -1
- package/public/assets/{analytics-a663ak56.js → analytics-D5OT5ajj.js} +2 -2
- package/public/assets/{analytics-a663ak56.js.map → analytics-D5OT5ajj.js.map} +1 -1
- package/public/assets/automation-Dm6rXNxK.js +2 -0
- package/public/assets/{automation-CiaLThdO.js.map → automation-Dm6rXNxK.js.map} +1 -1
- package/public/assets/{branch-state-badge-D4ur3m3_.js → branch-state-badge-FX5Yww2s.js} +2 -2
- package/public/assets/{branch-state-badge-D4ur3m3_.js.map → branch-state-badge-FX5Yww2s.js.map} +1 -1
- package/public/assets/{channels-o9KLTHoK.js → channels--rdAiX17.js} +2 -2
- package/public/assets/{channels-o9KLTHoK.js.map → channels--rdAiX17.js.map} +1 -1
- package/public/assets/chat-JZAEDGfX.js +2 -0
- package/public/assets/chat-JZAEDGfX.js.map +1 -0
- package/public/assets/{connectors-CdC806mA.js → connectors-Bx4gzvNf.js} +2 -2
- package/public/assets/{connectors-CdC806mA.js.map → connectors-Bx4gzvNf.js.map} +1 -1
- package/public/assets/display-Bebqs1qu.js +3 -0
- package/public/assets/display-Bebqs1qu.js.map +1 -0
- package/public/assets/{formatted-body-impl-Ca74OAEH.js → formatted-body-impl-CVq4qHix.js} +2 -2
- package/public/assets/{formatted-body-impl-Ca74OAEH.js.map → formatted-body-impl-CVq4qHix.js.map} +1 -1
- package/public/assets/{index-C_33ymaw.js → index-BHRtR4q7.js} +8 -8
- package/public/assets/{index-C_33ymaw.js.map → index-BHRtR4q7.js.map} +1 -1
- package/public/assets/{insights-ClI68s39.js → insights-yJFgCa3o.js} +2 -2
- package/public/assets/{insights-ClI68s39.js.map → insights-yJFgCa3o.js.map} +1 -1
- package/public/assets/{integrations-1nxMizDY.js → integrations-k1HIONjo.js} +2 -2
- package/public/assets/{integrations-1nxMizDY.js.map → integrations-k1HIONjo.js.map} +1 -1
- package/public/assets/maintenance-CsoOFBXx.js +2 -0
- package/public/assets/{maintenance-DiFNzNPN.js.map → maintenance-CsoOFBXx.js.map} +1 -1
- package/public/assets/{managed-agents-Do3dKvfj.js → managed-agents-Q3HuVjGg.js} +2 -2
- package/public/assets/{managed-agents-Do3dKvfj.js.map → managed-agents-Q3HuVjGg.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CLA0J255.js → markdown-preview-impl-CnsMjrnu.js} +2 -2
- package/public/assets/{markdown-preview-impl-CLA0J255.js.map → markdown-preview-impl-CnsMjrnu.js.map} +1 -1
- package/public/assets/{memory-IjwqFzBd.js → memory-D3-K5eJS.js} +2 -2
- package/public/assets/{memory-IjwqFzBd.js.map → memory-D3-K5eJS.js.map} +1 -1
- package/public/assets/{messages-DjvWqHyn.js → messages-B4lCP5rS.js} +2 -2
- package/public/assets/{messages-DjvWqHyn.js.map → messages-B4lCP5rS.js.map} +1 -1
- package/public/assets/{orchestrators-D2IqDxDT.js → orchestrators-CRoZtLeQ.js} +2 -2
- package/public/assets/{orchestrators-D2IqDxDT.js.map → orchestrators-CRoZtLeQ.js.map} +1 -1
- package/public/assets/{overview-DKC3TbAh.js → overview-CxCU2fOF.js} +2 -2
- package/public/assets/{overview-DKC3TbAh.js.map → overview-CxCU2fOF.js.map} +1 -1
- package/public/assets/pairs-unqjPlmq.js +2 -0
- package/public/assets/{pairs-WpKCPE1n.js.map → pairs-unqjPlmq.js.map} +1 -1
- package/public/assets/{security-BF7ZtPQe.js → security-B7HhSYNy.js} +2 -2
- package/public/assets/{security-BF7ZtPQe.js.map → security-B7HhSYNy.js.map} +1 -1
- package/public/assets/{settings-CQnjrTa-.js → settings-B9NDhsAb.js} +2 -2
- package/public/assets/{settings-CQnjrTa-.js.map → settings-B9NDhsAb.js.map} +1 -1
- package/public/assets/store-DiSzYHj9.js +9 -0
- package/public/assets/{store-C9VcSo05.js.map → store-DiSzYHj9.js.map} +1 -1
- package/public/assets/{tasks-CbN_GSSb.js → tasks-CIQolvNm.js} +2 -2
- package/public/assets/{tasks-CbN_GSSb.js.map → tasks-CIQolvNm.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-BJRohThT.js → terminal-viewer-impl-DCifVqFR.js} +2 -2
- package/public/assets/{terminal-viewer-impl-BJRohThT.js.map → terminal-viewer-impl-DCifVqFR.js.map} +1 -1
- package/public/assets/{work-queue-C5xLBLmm.js → work-queue-Dr3c1V6O.js} +2 -2
- package/public/assets/{work-queue-C5xLBLmm.js.map → work-queue-Dr3c1V6O.js.map} +1 -1
- package/public/assets/{workspaces-D91H3wDX.js → workspaces-B1Jxop7h.js} +3 -3
- package/public/assets/{workspaces-D91H3wDX.js.map → workspaces-B1Jxop7h.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/src/adapter.ts +1 -1
- package/src/agent-lifecycle-events.ts +137 -0
- package/src/artifact-storage.ts +3 -5
- package/src/branch-landed.ts +38 -2
- package/src/cli/_shared.ts +80 -0
- package/src/cli/agent-detect.ts +188 -0
- package/src/cli/agent-meta.ts +95 -0
- package/src/cli/context-probe.ts +88 -0
- package/src/cli/daemon.ts +111 -0
- package/src/cli/dev.ts +173 -0
- package/src/cli/index.ts +361 -0
- package/src/cli/introspect.ts +73 -0
- package/src/cli/memory.ts +37 -0
- package/src/cli/message.ts +201 -0
- package/src/cli/orchestrator.ts +227 -0
- package/src/cli/pair.ts +125 -0
- package/src/cli/provider.ts +209 -0
- package/src/cli/recipe.ts +110 -0
- package/src/cli/reply.ts +141 -0
- package/src/cli/setup.ts +57 -0
- package/src/cli/steward.ts +59 -0
- package/src/cli/token.ts +81 -0
- package/src/cli/upgrade.ts +193 -0
- package/src/cli/workspace.ts +215 -0
- package/src/cli.ts +4 -2718
- package/src/config-store.ts +10 -6
- package/src/maintenance.ts +25 -21
- package/src/mcp-errors.ts +7 -0
- package/src/mcp.ts +34 -36
- package/src/routes/agents-spawn.ts +9 -1
- package/src/routes/agents.ts +5 -0
- package/src/routes/commands.ts +15 -0
- package/src/routes/workspaces.ts +13 -4
- package/src/spawn-targets.ts +159 -0
- package/src/utils.ts +16 -1
- package/src/workspace-actions.ts +7 -1
- package/src/workspace-merge.ts +12 -1
- package/public/assets/automation-CiaLThdO.js +0 -2
- package/public/assets/chat-5hvHZcAe.js +0 -2
- package/public/assets/chat-5hvHZcAe.js.map +0 -1
- package/public/assets/display-JI19Vc7L.js +0 -3
- package/public/assets/display-JI19Vc7L.js.map +0 -1
- package/public/assets/maintenance-DiFNzNPN.js +0 -2
- package/public/assets/pairs-WpKCPE1n.js +0 -2
- package/public/assets/store-C9VcSo05.js +0 -9
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { emitRelayEvent } from "./events";
|
|
2
|
+
import { getNotificationsConfig } from "./config-store";
|
|
3
|
+
import { notifySystemMessage } from "./notify";
|
|
4
|
+
import { getAgent } from "./db";
|
|
5
|
+
import type { AgentCard } from "./types";
|
|
6
|
+
|
|
7
|
+
// #308/#239 — async spawn lifecycle signals. A spawn-capable agent fires a spawn, gets its
|
|
8
|
+
// spawnRequestId back immediately, and is WOKEN on ready/exit/failure instead of burning its
|
|
9
|
+
// turn block-polling `waitForRegistrationMs`. Mirrors src/branch-landed.ts: always emit the
|
|
10
|
+
// durable event onto the bus; gate only the agent-facing push (it wakes the recipient).
|
|
11
|
+
|
|
12
|
+
// Spawned children that have fired `agent.ready` during this relay lifetime. Serves two jobs:
|
|
13
|
+
// (1) dedup the ready push — it must fire exactly once per child, not on every heartbeat
|
|
14
|
+
// re-registration; and (2) classify a later offline transition: a child IN this set exited
|
|
15
|
+
// cleanly (`agent.exited`); one that never entered it went offline without ever becoming ready,
|
|
16
|
+
// i.e. a spawn that failed to come up (`agent.spawn_failed` — the "process started ≠ ready"
|
|
17
|
+
// crash-loop / onboarding-gate mode the isolated-Claude failure proves). In-memory by design: a
|
|
18
|
+
// child still alive across a relay restart was ready long ago and its parent already knows, so a
|
|
19
|
+
// post-restart exit reads as the benign `agent.exited` default — a restart must never manufacture
|
|
20
|
+
// a spurious failure alert.
|
|
21
|
+
const readyFired = new Set<string>();
|
|
22
|
+
|
|
23
|
+
function describe(child: AgentCard): string {
|
|
24
|
+
return child.label ? `${child.label} (\`${child.id}\`)` : `\`${child.id}\``;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function providerOf(child: AgentCard): string | undefined {
|
|
28
|
+
const p = child.meta?.provider;
|
|
29
|
+
return typeof p === "string" ? p : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function spawnRequestIdOf(child: AgentCard): string | undefined {
|
|
33
|
+
const s = child.meta?.spawnRequestId;
|
|
34
|
+
return typeof s === "string" ? s : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A spawned child became genuinely ready (registered AND past onboarding — ready+idle, not merely
|
|
39
|
+
* process-started). Emits the durable `agent.ready` event and, unless suppressed, pushes the
|
|
40
|
+
* `spawned_by` parent a one-shot notice so it can stop block-polling. No-op for non-spawned agents
|
|
41
|
+
* and for a child whose ready was already announced (reconnect / heartbeat re-registration).
|
|
42
|
+
*/
|
|
43
|
+
export function notifyAgentReady(childId: string): void {
|
|
44
|
+
const child = getAgent(childId);
|
|
45
|
+
const parent = child?.spawnedBy;
|
|
46
|
+
if (!child || !parent) return;
|
|
47
|
+
if (readyFired.has(childId)) return;
|
|
48
|
+
readyFired.add(childId);
|
|
49
|
+
|
|
50
|
+
emitRelayEvent({
|
|
51
|
+
type: "agent.ready",
|
|
52
|
+
source: "server",
|
|
53
|
+
subject: childId,
|
|
54
|
+
data: { agentId: childId, parent, provider: providerOf(child), label: child.label, spawnRequestId: spawnRequestIdOf(child) },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const config = getNotificationsConfig();
|
|
58
|
+
if (!config.enabled || !config.agentReady) return;
|
|
59
|
+
notifySystemMessage(parent, {
|
|
60
|
+
subject: "Spawned agent ready",
|
|
61
|
+
body: `✅ Your spawned agent ${describe(child)} is ready and idle — send it work with relay_send_message to \`${childId}\`.`,
|
|
62
|
+
payload: { kind: "agent.ready", agentId: childId, parent, provider: providerOf(child), spawnRequestId: spawnRequestIdOf(child) },
|
|
63
|
+
replyExpected: false,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A spawned child went offline (heartbeat-reaped, or shut down). Classifies via `readyFired`: a
|
|
69
|
+
* child that had become ready exited (`agent.exited`); one that never did failed to come up
|
|
70
|
+
* (`agent.spawn_failed`). Routes to the `spawned_by` parent. No-op for non-spawned agents.
|
|
71
|
+
* `reason` is the offline cause (e.g. "heartbeat lost", "shutdown").
|
|
72
|
+
*/
|
|
73
|
+
export function notifyAgentOffline(childId: string, reason: string): void {
|
|
74
|
+
const child = getAgent(childId);
|
|
75
|
+
const parent = child?.spawnedBy;
|
|
76
|
+
if (!child || !parent) return;
|
|
77
|
+
const wasReady = readyFired.delete(childId);
|
|
78
|
+
const config = getNotificationsConfig();
|
|
79
|
+
|
|
80
|
+
if (wasReady) {
|
|
81
|
+
emitRelayEvent({
|
|
82
|
+
type: "agent.exited",
|
|
83
|
+
source: "server",
|
|
84
|
+
subject: childId,
|
|
85
|
+
data: { agentId: childId, parent, reason, provider: providerOf(child), spawnRequestId: spawnRequestIdOf(child) },
|
|
86
|
+
});
|
|
87
|
+
if (!config.enabled || !config.agentExited) return;
|
|
88
|
+
notifySystemMessage(parent, {
|
|
89
|
+
subject: "Spawned agent exited",
|
|
90
|
+
body: `⏹ Your spawned agent ${describe(child)} exited — ${reason}.`,
|
|
91
|
+
payload: { kind: "agent.exited", agentId: childId, parent, reason },
|
|
92
|
+
replyExpected: false,
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
emitRelayEvent({
|
|
98
|
+
type: "agent.spawn_failed",
|
|
99
|
+
source: "server",
|
|
100
|
+
subject: childId,
|
|
101
|
+
data: { agentId: childId, parent, reason, provider: providerOf(child), spawnRequestId: spawnRequestIdOf(child), neverReady: true },
|
|
102
|
+
});
|
|
103
|
+
if (!config.enabled || !config.agentSpawnFailed) return;
|
|
104
|
+
notifySystemMessage(parent, {
|
|
105
|
+
subject: "Spawn failed",
|
|
106
|
+
body: `❌ Your spawned agent ${describe(child)} went offline before ever becoming ready — ${reason}. Likely a crash-loop, onboarding gate, or bad launch; call relay_spawn_targets to re-check provider availability before retrying.`,
|
|
107
|
+
payload: { kind: "agent.spawn_failed", agentId: childId, parent, reason },
|
|
108
|
+
replyExpected: false,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A spawn COMMAND failed outright — the orchestrator reported the `agent.spawn` command failed
|
|
114
|
+
* (provider unavailable, bad cwd, launch error) before any child agent registered. Routes directly
|
|
115
|
+
* to the spawning parent threaded on the command, since there is no child agent record to resolve.
|
|
116
|
+
*/
|
|
117
|
+
export function notifyAgentSpawnFailed(input: { parent: string; spawnRequestId?: string; provider?: string; reason: string }): void {
|
|
118
|
+
emitRelayEvent({
|
|
119
|
+
type: "agent.spawn_failed",
|
|
120
|
+
source: "server",
|
|
121
|
+
subject: input.spawnRequestId ?? input.parent,
|
|
122
|
+
data: { parent: input.parent, spawnRequestId: input.spawnRequestId, provider: input.provider, reason: input.reason },
|
|
123
|
+
});
|
|
124
|
+
const config = getNotificationsConfig();
|
|
125
|
+
if (!config.enabled || !config.agentSpawnFailed) return;
|
|
126
|
+
notifySystemMessage(input.parent, {
|
|
127
|
+
subject: "Spawn failed",
|
|
128
|
+
body: `❌ Your spawn request${input.spawnRequestId ? ` \`${input.spawnRequestId}\`` : ""} failed — ${input.reason}. Call relay_spawn_targets to check live provider/host availability before retrying.`,
|
|
129
|
+
payload: { kind: "agent.spawn_failed", parent: input.parent, spawnRequestId: input.spawnRequestId, provider: input.provider, reason: input.reason },
|
|
130
|
+
replyExpected: false,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Test seam: clear the in-memory ready-tracking set between tests. */
|
|
135
|
+
export function __resetReadyTracking(): void {
|
|
136
|
+
readyFired.clear();
|
|
137
|
+
}
|
package/src/artifact-storage.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { createReadStream, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { Readable } from "node:stream";
|
|
6
|
+
import { expandTilde } from "./utils";
|
|
6
7
|
|
|
7
8
|
interface ArtifactStorageBackend {
|
|
8
9
|
readonly scheme: string;
|
|
@@ -19,10 +20,7 @@ interface ArtifactStorageBackend {
|
|
|
19
20
|
const DEFAULT_MAX_ARTIFACT_BYTES = 50 * 1024 * 1024;
|
|
20
21
|
|
|
21
22
|
function artifactRoot(): string {
|
|
22
|
-
|
|
23
|
-
if (configured === "~") return homedir();
|
|
24
|
-
if (configured.startsWith("~/")) return join(homedir(), configured.slice(2));
|
|
25
|
-
return configured;
|
|
23
|
+
return expandTilde(process.env.AGENT_RELAY_ARTIFACTS_DIR || "~/.agent-relay/artifacts");
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
export function maxArtifactBytes(): number {
|
package/src/branch-landed.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { emitRelayEvent } from "./events";
|
|
2
2
|
import { getNotificationsConfig } from "./config-store";
|
|
3
3
|
import { notifySystemMessage } from "./notify";
|
|
4
|
-
import { listAgents } from "./db";
|
|
4
|
+
import { createActivityEvent, listAgents, updateWorkspaceStatus } from "./db";
|
|
5
5
|
import { isAgentOnline } from "./agent-ref";
|
|
6
|
-
import type { AgentCard, WorkspaceRecord } from "./types";
|
|
6
|
+
import type { AgentCard, WorkspaceMergePreview, WorkspaceRecord } from "./types";
|
|
7
7
|
|
|
8
8
|
export interface BranchLandedInput {
|
|
9
9
|
/**
|
|
@@ -113,6 +113,42 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Finalize a workspace whose work landed in base out-of-band — a pr-strategy land whose
|
|
118
|
+
* PR merged (by anyone, incl. `gh pr merge`), or a squash/cherry-pick already in base
|
|
119
|
+
* (#304). Marks it terminal `merged`, records the merge SHA, and fires the same
|
|
120
|
+
* branch.landed push the local land path emits — a pr land never runs through the
|
|
121
|
+
* workspace.merge result handler, so without this the author never learns it shipped.
|
|
122
|
+
* Lives here (not maintenance.ts) so the giant doesn't grow and land-notify has one home.
|
|
123
|
+
*/
|
|
124
|
+
export function reconcileLandedWorkspace(ws: WorkspaceRecord, preview: WorkspaceMergePreview): void {
|
|
125
|
+
const sha = typeof preview.prMergeSha === "string" ? preview.prMergeSha : undefined;
|
|
126
|
+
const via = preview.prMerged === true ? "pr" : "git";
|
|
127
|
+
updateWorkspaceStatus(ws.id, "merged", {
|
|
128
|
+
autoMerged: true,
|
|
129
|
+
mergedFromStatus: ws.status,
|
|
130
|
+
landedDetectedAt: Date.now(),
|
|
131
|
+
landedVia: via,
|
|
132
|
+
...(sha ? { landedSha: sha } : {}),
|
|
133
|
+
autoConflict: false,
|
|
134
|
+
});
|
|
135
|
+
createActivityEvent({
|
|
136
|
+
clientId: "server-workspace-" + ws.id + "-merged-" + Date.now(),
|
|
137
|
+
kind: "state",
|
|
138
|
+
title: "Workspace work landed in base",
|
|
139
|
+
body: `${ws.branch ?? ws.id} is ${via === "pr" ? "merged on the remote (PR)" : "already merged into base"} ${preview.baseRef ? `(${preview.baseRef})` : ""} — marking merged`,
|
|
140
|
+
meta: ws.branch ?? ws.id,
|
|
141
|
+
icon: "ti-git-merge",
|
|
142
|
+
view: "orchestrators",
|
|
143
|
+
metadata: { source: "server", maintenanceJobId: "workspace-conflict-scan", workspaceId: ws.id, fromStatus: ws.status, ...(sha ? { landedSha: sha } : {}) },
|
|
144
|
+
});
|
|
145
|
+
try {
|
|
146
|
+
notifyBranchLanded({ workspace: ws, mergedSha: sha, pushed: true });
|
|
147
|
+
} catch {
|
|
148
|
+
// Notification is best-effort; the merged status + activity event still stand.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
116
152
|
// An agent is "on `main`" when its registered cwd equals the repo's main checkout — i.e. it
|
|
117
153
|
// works in the base, not an isolated worktree. Excludes the author, pseudo agents (system/
|
|
118
154
|
// user), channels, and offline sessions.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Shared CLI infrastructure — auto-split from cli.ts (#294). Generic helpers used
|
|
2
|
+
// across the command modules: relay HTTP, confirmation prompt, stdin read, and
|
|
3
|
+
// small string utilities. Keep this dependency-free of the command modules so the
|
|
4
|
+
// import graph stays acyclic (command modules import from here, never the reverse).
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
7
|
+
import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
8
|
+
|
|
9
|
+
export async function apiRequest(method: string, path: string, body?: unknown): Promise<unknown> {
|
|
10
|
+
const baseUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
|
|
11
|
+
const headers: Record<string, string> = {};
|
|
12
|
+
const token = process.env.AGENT_RELAY_TOKEN;
|
|
13
|
+
if (token) headers[RELAY_TOKEN_HEADER] = token;
|
|
14
|
+
if (body !== undefined) headers["Content-Type"] = "application/json";
|
|
15
|
+
const response = await fetch(new URL(path, baseUrl), {
|
|
16
|
+
method,
|
|
17
|
+
headers,
|
|
18
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
19
|
+
});
|
|
20
|
+
const text = await response.text();
|
|
21
|
+
const payload = text ? JSON.parse(text) : null;
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const message = payload && typeof payload === "object" && "error" in payload ? String((payload as any).error) : text;
|
|
24
|
+
throw new Error(`agent-relay ${method} ${path} failed (${response.status}): ${message}`);
|
|
25
|
+
}
|
|
26
|
+
return payload;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function apiRawRequest(method: string, path: string, body: BodyInit, extraHeaders: Record<string, string> = {}): Promise<unknown> {
|
|
30
|
+
const baseUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
|
|
31
|
+
const headers: Record<string, string> = { ...extraHeaders };
|
|
32
|
+
const token = process.env.AGENT_RELAY_TOKEN;
|
|
33
|
+
if (token) headers[RELAY_TOKEN_HEADER] = token;
|
|
34
|
+
const response = await fetch(new URL(path, baseUrl), { method, headers, body });
|
|
35
|
+
const text = await response.text();
|
|
36
|
+
const payload = text ? JSON.parse(text) : null;
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const message = payload && typeof payload === "object" && "error" in payload ? String((payload as any).error) : text;
|
|
39
|
+
throw new Error(`agent-relay ${method} ${path} failed (${response.status}): ${message}`);
|
|
40
|
+
}
|
|
41
|
+
return payload;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function readStdin(): Promise<string> {
|
|
45
|
+
if (typeof Bun !== "undefined" && Bun.stdin && typeof Bun.stdin.text === "function") {
|
|
46
|
+
return Bun.stdin.text();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let value = "";
|
|
50
|
+
for await (const chunk of process.stdin) value += String(chunk);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function splitTagArgs(raw: string): string[] {
|
|
55
|
+
return raw.split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function uniqueStrings(values: string[]): string[] {
|
|
59
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function encodedLength(value: string): number {
|
|
63
|
+
return new TextEncoder().encode(value).byteLength;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function truncateText(value: string, maxChars: number): string {
|
|
67
|
+
if (value.length <= maxChars) return value;
|
|
68
|
+
return `${value.slice(0, maxChars)}\n\n[truncated preview; full content is attached]`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function confirm(message: string): Promise<boolean> {
|
|
72
|
+
if (!input.isTTY) return false;
|
|
73
|
+
const rl = createInterface({ input, output });
|
|
74
|
+
try {
|
|
75
|
+
const answer = await rl.question(`${message} [y/N] `);
|
|
76
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
77
|
+
} finally {
|
|
78
|
+
rl.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Agent-id detection — auto-split from cli.ts (#294). Resolves "who am I in the
|
|
2
|
+
// relay" from env, on-disk context/codex state files, the Claude instance marker,
|
|
3
|
+
// or a unique cwd-matched online agent. Shared by every command that needs the
|
|
4
|
+
// caller's agent id (pair, message, reply, react, status, label, tags, workspace).
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { apiRequest, uniqueStrings } from "./_shared";
|
|
8
|
+
|
|
9
|
+
export async function detectActivePairId(agentId: string): Promise<string | undefined> {
|
|
10
|
+
const pairs = await apiRequest("GET", `/api/pairs?agent=${encodeURIComponent(agentId)}&status=active`) as Array<{ id?: string }>;
|
|
11
|
+
return Array.isArray(pairs) && typeof pairs[0]?.id === "string" ? pairs[0].id : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function detectAgentId(): Promise<string | undefined> {
|
|
15
|
+
const explicit = process.env.AGENT_RELAY_ID;
|
|
16
|
+
if (explicit) return explicit;
|
|
17
|
+
|
|
18
|
+
const contextMatch = currentAgentContextId();
|
|
19
|
+
if (contextMatch) return contextMatch;
|
|
20
|
+
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
const explicitCodexState = process.env.AGENT_RELAY_CODEX_STATE_PATH
|
|
23
|
+
? readCodexState(process.env.AGENT_RELAY_CODEX_STATE_PATH)
|
|
24
|
+
: null;
|
|
25
|
+
if (explicitCodexState?.agentId) return explicitCodexState.agentId;
|
|
26
|
+
|
|
27
|
+
const stateCandidates = [
|
|
28
|
+
resolve(cwd, "codex/runtime/live-state.json"),
|
|
29
|
+
...collectCodexStateFiles(),
|
|
30
|
+
].filter((path): path is string => Boolean(path));
|
|
31
|
+
|
|
32
|
+
const codexMatch = newestCodexAgentId(stateCandidates, cwd);
|
|
33
|
+
if (codexMatch) return codexMatch;
|
|
34
|
+
|
|
35
|
+
const claudeMatch = currentClaudeAgentId();
|
|
36
|
+
if (claudeMatch) return claudeMatch;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const agents = await apiRequest("GET", "/api/agents") as Array<{ id?: string; status?: string; ready?: boolean; meta?: { cwd?: unknown }; lastSeen?: number }>;
|
|
40
|
+
const cwdAgents = agents
|
|
41
|
+
.filter((agent) => agent.status !== "offline" && agent.ready !== false && agent.meta?.cwd === cwd && typeof agent.id === "string")
|
|
42
|
+
.sort((a, b) => (b.lastSeen ?? 0) - (a.lastSeen ?? 0));
|
|
43
|
+
const uniqueAgentIds = uniqueStrings(cwdAgents.map((agent) => agent.id!));
|
|
44
|
+
return uniqueAgentIds.length === 1 ? uniqueAgentIds[0] : undefined;
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function currentAgentContextId(): string | undefined {
|
|
51
|
+
const explicitPath = process.env.AGENT_RELAY_CONTEXT_PATH;
|
|
52
|
+
if (explicitPath) {
|
|
53
|
+
const explicit = readAgentContext(explicitPath);
|
|
54
|
+
if (explicit?.agentId && contextMatchesCurrentProcess(explicit)) return explicit.agentId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const candidates = collectAgentContextFiles();
|
|
58
|
+
const matches = candidates
|
|
59
|
+
.map((path) => readAgentContext(path))
|
|
60
|
+
.filter((context): context is AgentContextState => Boolean(context))
|
|
61
|
+
.filter((context) => contextMatchesCurrentProcess(context))
|
|
62
|
+
.filter((context) => context.matchEnv.some((match) => process.env[match.name] === match.value))
|
|
63
|
+
.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
64
|
+
|
|
65
|
+
const uniqueAgentIds = uniqueStrings(matches.map((context) => context.agentId));
|
|
66
|
+
return uniqueAgentIds.length === 1 ? uniqueAgentIds[0] : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface AgentContextState {
|
|
70
|
+
agentId: string;
|
|
71
|
+
cwd?: string;
|
|
72
|
+
updatedAtMs: number;
|
|
73
|
+
matchEnv: Array<{ name: string; value: string }>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function contextMatchesCurrentProcess(context: AgentContextState): boolean {
|
|
77
|
+
return !context.cwd || context.cwd === process.cwd();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readAgentContext(path: string): AgentContextState | null {
|
|
81
|
+
if (!existsSync(path)) return null;
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as {
|
|
84
|
+
agentId?: unknown;
|
|
85
|
+
cwd?: unknown;
|
|
86
|
+
updatedAt?: unknown;
|
|
87
|
+
matchEnv?: unknown;
|
|
88
|
+
};
|
|
89
|
+
if (typeof parsed.agentId !== "string" || !parsed.agentId) return null;
|
|
90
|
+
const matchEnv = Array.isArray(parsed.matchEnv)
|
|
91
|
+
? parsed.matchEnv.flatMap((item) => {
|
|
92
|
+
if (!item || typeof item !== "object") return [];
|
|
93
|
+
const record = item as { name?: unknown; value?: unknown };
|
|
94
|
+
return typeof record.name === "string" && typeof record.value === "string"
|
|
95
|
+
? [{ name: record.name, value: record.value }]
|
|
96
|
+
: [];
|
|
97
|
+
})
|
|
98
|
+
: [];
|
|
99
|
+
const stat = statSync(path);
|
|
100
|
+
const updatedAt = typeof parsed.updatedAt === "string" ? Date.parse(parsed.updatedAt) : Number.NaN;
|
|
101
|
+
return {
|
|
102
|
+
agentId: parsed.agentId,
|
|
103
|
+
cwd: typeof parsed.cwd === "string" ? parsed.cwd : undefined,
|
|
104
|
+
matchEnv,
|
|
105
|
+
updatedAtMs: Number.isFinite(updatedAt) ? updatedAt : stat.mtimeMs,
|
|
106
|
+
};
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function collectAgentContextFiles(): string[] {
|
|
113
|
+
const roots = [
|
|
114
|
+
join(process.env.HOME || "", ".agent-relay", "contexts"),
|
|
115
|
+
].filter((root) => root && existsSync(root));
|
|
116
|
+
const files: string[] = [];
|
|
117
|
+
for (const root of roots) collectFiles(root, ".json", files, 2);
|
|
118
|
+
return files;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function newestCodexAgentId(paths: string[], cwd: string): string | undefined {
|
|
122
|
+
const states = paths
|
|
123
|
+
.map((path) => readCodexState(path))
|
|
124
|
+
.filter((state): state is { agentId: string; cwd?: string; updatedAtMs: number } => Boolean(state))
|
|
125
|
+
.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
126
|
+
const cwdAgentIds = uniqueStrings(states.filter((state) => state.cwd === cwd).map((state) => state.agentId));
|
|
127
|
+
return cwdAgentIds.length === 1 ? cwdAgentIds[0] : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readCodexState(path: string): { agentId: string; cwd?: string; updatedAtMs: number } | null {
|
|
131
|
+
if (!existsSync(path)) return null;
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as { agentId?: unknown; cwd?: unknown; updatedAt?: unknown };
|
|
134
|
+
if (typeof parsed.agentId !== "string" || !parsed.agentId) return null;
|
|
135
|
+
const stat = statSync(path);
|
|
136
|
+
const updatedAt = typeof parsed.updatedAt === "string" ? Date.parse(parsed.updatedAt) : Number.NaN;
|
|
137
|
+
return {
|
|
138
|
+
agentId: parsed.agentId,
|
|
139
|
+
cwd: typeof parsed.cwd === "string" ? parsed.cwd : undefined,
|
|
140
|
+
updatedAtMs: Number.isFinite(updatedAt) ? updatedAt : stat.mtimeMs,
|
|
141
|
+
};
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function collectCodexStateFiles(): string[] {
|
|
148
|
+
const roots = [
|
|
149
|
+
join(process.env.HOME || "", ".agent-relay", "codex", "runtime"),
|
|
150
|
+
resolve(process.cwd(), "codex", "runtime"),
|
|
151
|
+
].filter((root) => root && existsSync(root));
|
|
152
|
+
const files: string[] = [];
|
|
153
|
+
for (const root of roots) collectFiles(root, "live-state.json", files, 4);
|
|
154
|
+
return files;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function collectFiles(dir: string, name: string, output: string[], depth: number): void {
|
|
158
|
+
if (depth < 0) return;
|
|
159
|
+
let entries: string[];
|
|
160
|
+
try {
|
|
161
|
+
entries = readdirSync(dir);
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
const path = join(dir, entry);
|
|
167
|
+
try {
|
|
168
|
+
const stat = statSync(path);
|
|
169
|
+
if (stat.isDirectory()) collectFiles(path, name, output, depth - 1);
|
|
170
|
+
else if (name.startsWith(".") ? entry.endsWith(name) : entry === name) output.push(path);
|
|
171
|
+
} catch {
|
|
172
|
+
// Ignore state files that disappear while scanning.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function currentClaudeAgentId(): string | undefined {
|
|
178
|
+
const sessionKey = process.env.CLAUDE_CODE_SESSION_ID || String(process.ppid || "");
|
|
179
|
+
if (!sessionKey) return undefined;
|
|
180
|
+
const safeSessionKey = sessionKey.replace(/[^A-Za-z0-9_.:-]/g, "_");
|
|
181
|
+
const statePath = join("/tmp", `agent-relay-instance-${safeSessionKey}.state`);
|
|
182
|
+
if (!existsSync(statePath)) return undefined;
|
|
183
|
+
try {
|
|
184
|
+
return readFileSync(statePath, "utf8").split(/\r?\n/)[0]?.trim() || undefined;
|
|
185
|
+
} catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Agent-metadata commands — auto-split from cli.ts (#294). Small handlers that
|
|
2
|
+
// read/update the caller's own agent record: status, label, tags.
|
|
3
|
+
import { apiRequest, splitTagArgs, uniqueStrings } from "./_shared";
|
|
4
|
+
import { detectAgentId } from "./agent-detect";
|
|
5
|
+
|
|
6
|
+
export async function handleStatusCommand(args: string[]): Promise<void> {
|
|
7
|
+
let agentId = await detectAgentId();
|
|
8
|
+
let json = false;
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
const arg = args[i];
|
|
11
|
+
if (arg === "--agent" && i + 1 < args.length) agentId = args[++i];
|
|
12
|
+
else if (arg === "--json") json = true;
|
|
13
|
+
else throw new Error(`Unknown status option "${arg}"`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const [stats, health, pairs, agent] = await Promise.all([
|
|
17
|
+
apiRequest("GET", "/api/stats"),
|
|
18
|
+
apiRequest("GET", "/api/health"),
|
|
19
|
+
agentId ? apiRequest("GET", `/api/pairs?agent=${encodeURIComponent(agentId)}`) : Promise.resolve([]),
|
|
20
|
+
agentId ? apiRequest("GET", `/api/agents/${encodeURIComponent(agentId)}`).catch(() => null) : Promise.resolve(null),
|
|
21
|
+
]);
|
|
22
|
+
const payload = { agent, stats, health, pairs };
|
|
23
|
+
if (json) console.log(JSON.stringify(payload, null, 2));
|
|
24
|
+
else console.log(formatStatus(payload));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function handleLabelCommand(args: string[]): Promise<void> {
|
|
28
|
+
let agentId = await detectAgentId();
|
|
29
|
+
let label: string | null | undefined;
|
|
30
|
+
let json = false;
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
const arg = args[i];
|
|
33
|
+
if (arg === "--agent" && i + 1 < args.length) agentId = args[++i];
|
|
34
|
+
else if (arg === "--clear") label = null;
|
|
35
|
+
else if (arg === "--json") json = true;
|
|
36
|
+
else if (label === undefined) label = args.slice(i).join(" ");
|
|
37
|
+
}
|
|
38
|
+
if (!agentId) throw new Error("Could not detect current Agent Relay ID. Pass --agent AGENT_ID or set AGENT_RELAY_ID.");
|
|
39
|
+
if (label === undefined) {
|
|
40
|
+
const agent = await apiRequest("GET", `/api/agents/${encodeURIComponent(agentId)}`) as { label?: string };
|
|
41
|
+
console.log(agent.label ?? "(no label)");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const result = await apiRequest("PATCH", `/api/agents/${encodeURIComponent(agentId)}/label`, { label });
|
|
45
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
46
|
+
else console.log(label ? `Label set: ${label}` : "Label cleared.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function handleTagsCommand(args: string[]): Promise<void> {
|
|
50
|
+
let agentId = await detectAgentId();
|
|
51
|
+
let json = false;
|
|
52
|
+
let listOnly = false;
|
|
53
|
+
let add: string[] = [];
|
|
54
|
+
let remove: string[] = [];
|
|
55
|
+
const positional: string[] = [];
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
const arg = args[i];
|
|
58
|
+
if (arg === "--agent" && i + 1 < args.length) agentId = args[++i];
|
|
59
|
+
else if (arg === "--json") json = true;
|
|
60
|
+
else if (arg === "--list") listOnly = true;
|
|
61
|
+
else if (arg === "--add" && i + 1 < args.length) add = add.concat(splitTagArgs(args[++i]!));
|
|
62
|
+
else if (arg === "--remove" && i + 1 < args.length) remove = remove.concat(splitTagArgs(args[++i]!));
|
|
63
|
+
else positional.push(...splitTagArgs(arg!));
|
|
64
|
+
}
|
|
65
|
+
if (!agentId) throw new Error("Could not detect current Agent Relay ID. Pass --agent AGENT_ID or set AGENT_RELAY_ID.");
|
|
66
|
+
const current = await apiRequest("GET", `/api/agents/${encodeURIComponent(agentId)}`) as { tags?: string[] };
|
|
67
|
+
if (listOnly || (positional.length === 0 && add.length === 0 && remove.length === 0)) {
|
|
68
|
+
console.log((current.tags ?? []).join(", ") || "(no tags)");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const next = positional.length > 0
|
|
72
|
+
? uniqueStrings(positional)
|
|
73
|
+
: uniqueStrings([...(current.tags ?? []), ...add]).filter((tag) => !remove.includes(tag));
|
|
74
|
+
const updated = await apiRequest("PATCH", `/api/agents/${encodeURIComponent(agentId)}/tags`, { tags: next });
|
|
75
|
+
if (json) console.log(JSON.stringify(updated, null, 2));
|
|
76
|
+
else console.log(`Tags: ${next.join(", ") || "(none)"}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatStatus(payload: any): string {
|
|
80
|
+
const agent = payload.agent;
|
|
81
|
+
const stats = payload.stats ?? {};
|
|
82
|
+
const health = payload.health ?? {};
|
|
83
|
+
const pairs = Array.isArray(payload.pairs) ? payload.pairs : [];
|
|
84
|
+
const activePair = pairs.find((pair: any) => pair.status === "active") ?? pairs.find((pair: any) => pair.status === "pending");
|
|
85
|
+
return [
|
|
86
|
+
`Relay: ${health.status ?? "unknown"} version=${stats.version ?? "unknown"}`,
|
|
87
|
+
`Agents: ${stats.online ?? "?"}/${stats.agents ?? "?"} online Messages: ${stats.messages ?? "?"} Tasks: ${stats.openTasks ?? "?"}/${stats.tasks ?? "?"} open`,
|
|
88
|
+
agent
|
|
89
|
+
? `Current: ${agent.id} status=${agent.status} ready=${agent.ready ? "yes" : "no"} label=${agent.label ?? "(none)"} tags=${(agent.tags ?? []).join(", ") || "(none)"}`
|
|
90
|
+
: "Current: unknown",
|
|
91
|
+
activePair
|
|
92
|
+
? `Pair: ${activePair.id} ${activePair.status} ${activePair.requesterId} <-> ${activePair.targetId}`
|
|
93
|
+
: "Pair: none active",
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Context-probe command — auto-split from cli.ts (#294). Wraps a status-line
|
|
2
|
+
// command (e.g. ccstatusline) so the relay can observe context usage, or emits the
|
|
3
|
+
// status-line invocation for the provider settings.
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { runContextProbe } from "agent-relay-sdk/context-probe";
|
|
8
|
+
import { shellQuote } from "agent-relay-sdk/shell-utils";
|
|
9
|
+
import { readStdin } from "./_shared";
|
|
10
|
+
|
|
11
|
+
export async function handleContextProbeCommand(args: string[]): Promise<void> {
|
|
12
|
+
const printStatusLine = args[0] === "print-status-line";
|
|
13
|
+
const inputArgs = printStatusLine ? args.slice(1) : args;
|
|
14
|
+
let wrapCommand: string | undefined;
|
|
15
|
+
let wrapRequested = false;
|
|
16
|
+
let agentId: string | undefined;
|
|
17
|
+
let stateDir: string | undefined;
|
|
18
|
+
let standalone = false;
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < inputArgs.length; i++) {
|
|
21
|
+
const arg = inputArgs[i];
|
|
22
|
+
if (arg === "--wrap") {
|
|
23
|
+
wrapRequested = true;
|
|
24
|
+
const next = inputArgs[i + 1];
|
|
25
|
+
if (next && !next.startsWith("--")) {
|
|
26
|
+
wrapCommand = next;
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
} else if (arg === "--agent-id") {
|
|
30
|
+
agentId = inputArgs[++i];
|
|
31
|
+
} else if (arg === "--state-dir") {
|
|
32
|
+
stateDir = inputArgs[++i] ?? stateDir;
|
|
33
|
+
} else if (arg === "--standalone") {
|
|
34
|
+
standalone = true;
|
|
35
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
36
|
+
console.log("Usage: agent-relay context-probe [print-status-line] [--wrap COMMAND] [--agent-id ID] [--state-dir DIR] [--standalone]");
|
|
37
|
+
return;
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error(`Unknown context-probe option "${arg}"`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (printStatusLine) {
|
|
44
|
+
const command = [
|
|
45
|
+
"agent-relay",
|
|
46
|
+
"context-probe",
|
|
47
|
+
...(wrapRequested ? ["--wrap", ...(wrapCommand ? [shellQuote(wrapCommand)] : [])] : ["--standalone"]),
|
|
48
|
+
...(agentId ? ["--agent-id", shellQuote(agentId)] : []),
|
|
49
|
+
...(stateDir ? ["--state-dir", shellQuote(stateDir)] : []),
|
|
50
|
+
].join(" ");
|
|
51
|
+
console.log(command);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (wrapRequested && !wrapCommand) wrapCommand = currentClaudeStatusLineCommand();
|
|
56
|
+
if (wrapCommand && commandLooksLikeContextProbe(wrapCommand)) {
|
|
57
|
+
wrapCommand = undefined;
|
|
58
|
+
standalone = true;
|
|
59
|
+
}
|
|
60
|
+
if (!wrapRequested && !standalone) {
|
|
61
|
+
throw new Error("Usage: agent-relay context-probe [--wrap COMMAND|--standalone] [--agent-id ID] [--state-dir DIR]");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const input = await readStdin();
|
|
65
|
+
const result = runContextProbe(input, { wrapCommand, agentId, stateDir, standalone });
|
|
66
|
+
if (result.wrappedStderr) process.stderr.write(result.wrappedStderr);
|
|
67
|
+
if (result.output) process.stdout.write(result.output);
|
|
68
|
+
if (result.output && !result.output.endsWith("\n")) process.stdout.write("\n");
|
|
69
|
+
if (typeof result.wrappedExitCode === "number" && result.wrappedExitCode !== 0) process.exit(result.wrappedExitCode);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function currentClaudeStatusLineCommand(): string | undefined {
|
|
73
|
+
const settingsPath = join(process.env.HOME || homedir(), ".claude", "settings.json");
|
|
74
|
+
try {
|
|
75
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as unknown;
|
|
76
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) return undefined;
|
|
77
|
+
const statusLine = (settings as Record<string, unknown>).statusLine;
|
|
78
|
+
if (!statusLine || typeof statusLine !== "object" || Array.isArray(statusLine)) return undefined;
|
|
79
|
+
const command = (statusLine as Record<string, unknown>).command;
|
|
80
|
+
return typeof command === "string" && command.trim() ? command : undefined;
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function commandLooksLikeContextProbe(command: string): boolean {
|
|
87
|
+
return /\bagent-relay\s+context-probe\b/.test(command);
|
|
88
|
+
}
|