@phnx-labs/agents-cli 1.22.24 → 1.22.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +350 -1
- package/README.md +31 -6
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/browser.js +35 -0
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +108 -9
- package/dist/commands/exec.js +19 -16
- package/dist/commands/feed.d.ts +5 -0
- package/dist/commands/feed.js +21 -1
- package/dist/commands/focus.js +2 -2
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +107 -33
- package/dist/commands/menubar.js +8 -0
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +72 -49
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions-browser.d.ts +1 -1
- package/dist/commands/sessions-browser.js +27 -7
- package/dist/commands/sessions-resume.js +3 -2
- package/dist/commands/sessions.d.ts +13 -1
- package/dist/commands/sessions.js +28 -2
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +106 -47
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -2
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +37 -2
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +155 -23
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/cloud/cursor.d.ts +79 -0
- package/dist/lib/cloud/cursor.js +228 -0
- package/dist/lib/cloud/registry.js +2 -0
- package/dist/lib/cloud/types.d.ts +7 -2
- package/dist/lib/cloud/types.js +14 -0
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/config.d.ts +7 -8
- package/dist/lib/crabbox/config.js +14 -14
- package/dist/lib/crabbox/lease.d.ts +11 -4
- package/dist/lib/crabbox/lease.js +40 -8
- package/dist/lib/crabbox/setup-copy.d.ts +5 -0
- package/dist/lib/crabbox/setup-copy.js +17 -1
- package/dist/lib/daemon.js +27 -1
- package/dist/lib/device-config.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +13 -1
- package/dist/lib/devices/doctor-findings.js +73 -2
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +3 -3
- package/dist/lib/exec.js +24 -10
- package/dist/lib/feed-outcome.d.ts +3 -0
- package/dist/lib/feed-outcome.js +18 -10
- package/dist/lib/feed.d.ts +4 -0
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +183 -6
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hosts/passthrough.d.ts +44 -0
- package/dist/lib/hosts/passthrough.js +84 -12
- package/dist/lib/hosts/ready.d.ts +2 -0
- package/dist/lib/hosts/ready.js +10 -1
- package/dist/lib/hosts/reconnect.d.ts +14 -12
- package/dist/lib/hosts/reconnect.js +41 -40
- package/dist/lib/mcp.js +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +0 -3
- package/dist/lib/runner.js +6 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.js +1 -20
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +13 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.d.ts +21 -3
- package/dist/lib/session/db.js +229 -15
- package/dist/lib/session/discover.d.ts +1 -0
- package/dist/lib/session/discover.js +115 -19
- package/dist/lib/session/insights.d.ts +18 -0
- package/dist/lib/session/insights.js +143 -1
- package/dist/lib/session/remote-list.d.ts +2 -0
- package/dist/lib/session/remote-list.js +1 -0
- package/dist/lib/session/session-cache.d.ts +4 -4
- package/dist/lib/session/session-cache.js +4 -4
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/shims.js +21 -1
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/ssh-exec.js +8 -2
- package/dist/lib/startup/command-registry.d.ts +16 -0
- package/dist/lib/startup/command-registry.js +46 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +76 -8
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/versions.js +16 -1
- package/dist/lib/watchdog/service.d.ts +17 -0
- package/dist/lib/watchdog/service.js +39 -0
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createInterface } from 'readline';
|
|
2
|
+
import { connectBrowserIPC } from './ipc.js';
|
|
3
|
+
import { assertRemoteControlAllowed } from './remote-control.js';
|
|
4
|
+
function parseRequest(line) {
|
|
5
|
+
const parsed = JSON.parse(line);
|
|
6
|
+
if (!parsed ||
|
|
7
|
+
typeof parsed !== 'object' ||
|
|
8
|
+
Array.isArray(parsed) ||
|
|
9
|
+
typeof parsed.action !== 'string') {
|
|
10
|
+
throw new Error('Each input line must be a JSON object with an action');
|
|
11
|
+
}
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
function writeResponse(output, response) {
|
|
15
|
+
output.write(`${JSON.stringify(response)}\n`);
|
|
16
|
+
}
|
|
17
|
+
function writeErrorResponse(output, error) {
|
|
18
|
+
writeResponse(output, {
|
|
19
|
+
ok: false,
|
|
20
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read browser IPC requests as NDJSON and write one NDJSON response per line.
|
|
25
|
+
* The Node process and daemon connection stay alive until input closes.
|
|
26
|
+
*/
|
|
27
|
+
export async function runBrowserIPCStream(options) {
|
|
28
|
+
const client = await connectBrowserIPC({ autoStartDaemon: options.autoStartDaemon });
|
|
29
|
+
const lines = createInterface({ input: options.input, crlfDelay: Infinity });
|
|
30
|
+
let defaultTask = options.task;
|
|
31
|
+
try {
|
|
32
|
+
for await (const line of lines) {
|
|
33
|
+
if (!line.trim())
|
|
34
|
+
continue;
|
|
35
|
+
let request;
|
|
36
|
+
try {
|
|
37
|
+
request = parseRequest(line);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
writeErrorResponse(options.output, error);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (request.action === 'start') {
|
|
44
|
+
try {
|
|
45
|
+
assertRemoteControlAllowed();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
writeErrorResponse(options.output, error);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
request = {
|
|
52
|
+
...request,
|
|
53
|
+
taskName: request.taskName ?? defaultTask,
|
|
54
|
+
actor: request.actor ?? options.actor,
|
|
55
|
+
launchId: request.launchId ?? options.launchId,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
else if (!request.task && defaultTask) {
|
|
59
|
+
request = { ...request, task: defaultTask };
|
|
60
|
+
}
|
|
61
|
+
const response = await client.request(request);
|
|
62
|
+
if (response.task)
|
|
63
|
+
defaultTask = response.task;
|
|
64
|
+
writeResponse(options.output, response);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
lines.close();
|
|
69
|
+
await client.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Meta } from '../types.js';
|
|
2
|
+
/** Why the owner sink cannot deliver from this box. */
|
|
3
|
+
export type OwnerSinkReason = 'rush-not-on-path' | 'rush-signed-out';
|
|
4
|
+
export interface OwnerSinkStatus {
|
|
5
|
+
/** Owner delivery is configured for this fleet (humans.yaml / notify.owner).
|
|
6
|
+
* When false, no finding is emitted — an un-opted-in box is not "broken". */
|
|
7
|
+
configured: boolean;
|
|
8
|
+
/** This box can actually deliver an owner notification right now. */
|
|
9
|
+
reachable: boolean;
|
|
10
|
+
/** Resolved owner channel (e.g. `imessage`). */
|
|
11
|
+
channel?: string;
|
|
12
|
+
/** Resolved transport after `notify.transports` mapping (usually === channel). */
|
|
13
|
+
transport?: string;
|
|
14
|
+
/** Set only when `reachable` is false. */
|
|
15
|
+
reason?: OwnerSinkReason;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Probe whether THIS box can deliver an owner notification right now. Returns
|
|
19
|
+
* `configured:false` (and the caller emits no finding) when the fleet has no
|
|
20
|
+
* owner channel configured — an un-opted-in box is not broken. When configured,
|
|
21
|
+
* reports whether the resolved transport can actually deliver from here.
|
|
22
|
+
*
|
|
23
|
+
* Only the concrete rush-backed failure the lane hits is reported as unreachable;
|
|
24
|
+
* non-rush transports (desktop / mailbox / …) deliver locally and are treated as
|
|
25
|
+
* reachable rather than probed, so this never invents a critical it cannot back.
|
|
26
|
+
*/
|
|
27
|
+
export declare function probeOwnerSink(meta: Meta): Promise<OwnerSinkStatus>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-delivery-sink reachability probe (RUSH-2262).
|
|
3
|
+
*
|
|
4
|
+
* The feed/notify owner-delivery lane (`agents notify`, `agents feed post
|
|
5
|
+
* --level important` / `--blocked`) reaches the owner over the rush-backed owner
|
|
6
|
+
* channel (iMessage via `rush message send`). That transport can only deliver
|
|
7
|
+
* from a context that BOTH finds the `rush` CLI on PATH and can read its
|
|
8
|
+
* keychain-bound session. So a headless Linux fleet box (no rush) or a non-GUI
|
|
9
|
+
* SSH session on a mac (login keychain locked) structurally cannot escalate —
|
|
10
|
+
* and the failure is silent until a block is filed, surfacing only as the
|
|
11
|
+
* after-the-fact `owner failed: …` line. `agents doctor` had no signal for it,
|
|
12
|
+
* which is exactly the gap RUSH-2258 / RUSH-2262 flagged.
|
|
13
|
+
*
|
|
14
|
+
* This probes the SAME transport the lane uses, from the SAME context doctor runs
|
|
15
|
+
* in, so `agents doctor` can fail loud when this box cannot reach the owner. It is
|
|
16
|
+
* deliberately honest about context: `rush whoami` is what tells a real signed-in
|
|
17
|
+
* session apart from a keychain that is present but unreadable HERE. The session
|
|
18
|
+
* token is a keychain item, NOT `~/.rush/user.yaml` (RUSH-2262), so this never
|
|
19
|
+
* reads that file — checking it is the mistake that made a signed-in box look
|
|
20
|
+
* signed out.
|
|
21
|
+
*
|
|
22
|
+
* `agents notify --dry-run` is NOT this probe: it short-circuits before the
|
|
23
|
+
* `which rush` preflight (`providers/rush.ts`), so it reports `ok:true` on a box
|
|
24
|
+
* with no rush at all. Resolvability (does the envelope build?) and reachability
|
|
25
|
+
* (can this box actually deliver?) are different questions; this answers the
|
|
26
|
+
* second.
|
|
27
|
+
*/
|
|
28
|
+
import { execFile } from 'child_process';
|
|
29
|
+
import { promisify } from 'util';
|
|
30
|
+
import { readOwnerDest } from './send.js';
|
|
31
|
+
import { RUSH_CHANNELS } from './providers/rush.js';
|
|
32
|
+
const execFileAsync = promisify(execFile);
|
|
33
|
+
async function rushOnPath() {
|
|
34
|
+
try {
|
|
35
|
+
await execFileAsync('which', ['rush'], { timeout: 4000 });
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Signed-in state from `rush whoami`, read with THIS context's keychain access.
|
|
43
|
+
* 'unknown' (a timeout or output we can't classify) is treated as reachable by
|
|
44
|
+
* the caller — a slow or unexpected probe must not cry wolf and paint a false
|
|
45
|
+
* critical. Only a definitive "not logged in" is reported as signed out. */
|
|
46
|
+
async function rushSignedIn() {
|
|
47
|
+
const classify = (s) => {
|
|
48
|
+
const out = s.toLowerCase();
|
|
49
|
+
if (/not (logged in|signed in)/.test(out))
|
|
50
|
+
return 'no';
|
|
51
|
+
if (/logged in as|session:\s*valid/.test(out))
|
|
52
|
+
return 'yes';
|
|
53
|
+
return 'unknown';
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
const { stdout, stderr } = await execFileAsync('rush', ['whoami'], { timeout: 5000 });
|
|
57
|
+
return classify(`${stdout}\n${stderr}`);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const e = err;
|
|
61
|
+
if (e.killed)
|
|
62
|
+
return 'unknown'; // timed out — do not conclude signed-out
|
|
63
|
+
// A signed-out rush commonly exits non-zero; trust an explicit message only.
|
|
64
|
+
return classify(`${e.stdout ?? ''}\n${e.stderr ?? ''}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Probe whether THIS box can deliver an owner notification right now. Returns
|
|
69
|
+
* `configured:false` (and the caller emits no finding) when the fleet has no
|
|
70
|
+
* owner channel configured — an un-opted-in box is not broken. When configured,
|
|
71
|
+
* reports whether the resolved transport can actually deliver from here.
|
|
72
|
+
*
|
|
73
|
+
* Only the concrete rush-backed failure the lane hits is reported as unreachable;
|
|
74
|
+
* non-rush transports (desktop / mailbox / …) deliver locally and are treated as
|
|
75
|
+
* reachable rather than probed, so this never invents a critical it cannot back.
|
|
76
|
+
*/
|
|
77
|
+
export async function probeOwnerSink(meta) {
|
|
78
|
+
const dest = readOwnerDest(meta);
|
|
79
|
+
if (!dest)
|
|
80
|
+
return { configured: false, reachable: false };
|
|
81
|
+
const channel = dest.channel;
|
|
82
|
+
const transport = meta.notify?.transports?.[channel] ?? channel;
|
|
83
|
+
if (RUSH_CHANNELS.includes(transport)) {
|
|
84
|
+
if (!(await rushOnPath())) {
|
|
85
|
+
return { configured: true, reachable: false, channel, transport, reason: 'rush-not-on-path' };
|
|
86
|
+
}
|
|
87
|
+
if ((await rushSignedIn()) === 'no') {
|
|
88
|
+
return { configured: true, reachable: false, channel, transport, reason: 'rush-signed-out' };
|
|
89
|
+
}
|
|
90
|
+
return { configured: true, reachable: true, channel, transport };
|
|
91
|
+
}
|
|
92
|
+
return { configured: true, reachable: true, channel, transport };
|
|
93
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Cursor Cloud Agents provider backed by the public v1 REST API. */
|
|
2
|
+
import type { CloudEvent, CloudProvider, CloudTask, CloudTaskStatus, DispatchOptions, ProviderCapabilities } from './types.js';
|
|
3
|
+
export declare const CURSOR_API_BASE_URL = "https://api.cursor.com/v1";
|
|
4
|
+
interface CursorAgent {
|
|
5
|
+
id: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
status: 'ACTIVE' | 'ARCHIVED';
|
|
8
|
+
createdAt: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
latestRunId?: string;
|
|
11
|
+
repos?: Array<{
|
|
12
|
+
url: string;
|
|
13
|
+
startingRef?: string;
|
|
14
|
+
prUrl?: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
interface CursorRun {
|
|
18
|
+
id: string;
|
|
19
|
+
agentId: string;
|
|
20
|
+
status: string;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
result?: string;
|
|
24
|
+
git?: {
|
|
25
|
+
branches?: Array<{
|
|
26
|
+
repoUrl: string;
|
|
27
|
+
branch?: string;
|
|
28
|
+
prUrl?: string;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface CursorCreateBody {
|
|
33
|
+
prompt: {
|
|
34
|
+
text: string;
|
|
35
|
+
images?: Array<{
|
|
36
|
+
data: string;
|
|
37
|
+
mimeType: string;
|
|
38
|
+
}>;
|
|
39
|
+
};
|
|
40
|
+
model?: {
|
|
41
|
+
id: string;
|
|
42
|
+
};
|
|
43
|
+
repos?: Array<{
|
|
44
|
+
url: string;
|
|
45
|
+
startingRef?: string;
|
|
46
|
+
}>;
|
|
47
|
+
autoCreatePR?: boolean;
|
|
48
|
+
envVars?: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
/** Translate unified dispatch options into Cursor's documented v1 create shape. */
|
|
51
|
+
export declare function buildCursorCreateBody(options: DispatchOptions): CursorCreateBody;
|
|
52
|
+
/** Map one Cursor agent/run pair into the provider-neutral task shape. */
|
|
53
|
+
export declare function parseCursorTask(agent: CursorAgent, run: CursorRun, prompt?: string): CloudTask;
|
|
54
|
+
/** Parse one complete SSE frame into a shared cloud event. */
|
|
55
|
+
export declare function parseCursorSseFrame(frame: string): CloudEvent | undefined;
|
|
56
|
+
/** Turn Cursor's structured API errors into actionable provider errors. */
|
|
57
|
+
export declare function cursorApiError(action: string, status: number, text: string): Error;
|
|
58
|
+
export declare class CursorCloudProvider implements CloudProvider {
|
|
59
|
+
id: "cursor";
|
|
60
|
+
name: string;
|
|
61
|
+
private secretsBundle?;
|
|
62
|
+
constructor(config?: {
|
|
63
|
+
secretsBundle?: string;
|
|
64
|
+
});
|
|
65
|
+
private hasKeySource;
|
|
66
|
+
private resolveApiKey;
|
|
67
|
+
capabilities(): ProviderCapabilities;
|
|
68
|
+
private request;
|
|
69
|
+
private latestRun;
|
|
70
|
+
dispatch(options: DispatchOptions): Promise<CloudTask>;
|
|
71
|
+
status(taskId: string): Promise<CloudTask>;
|
|
72
|
+
list(filter?: {
|
|
73
|
+
status?: CloudTaskStatus;
|
|
74
|
+
}): Promise<CloudTask[]>;
|
|
75
|
+
stream(taskId: string): AsyncIterable<CloudEvent>;
|
|
76
|
+
cancel(taskId: string): Promise<void>;
|
|
77
|
+
message(taskId: string, content: string): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/** Cursor Cloud Agents provider backed by the public v1 REST API. */
|
|
2
|
+
import { normalizeProviderStatus, resolveDispatchRepos } from './types.js';
|
|
3
|
+
import { readAndResolveBundleEnv } from '../secrets/bundles.js';
|
|
4
|
+
export const CURSOR_API_BASE_URL = 'https://api.cursor.com/v1';
|
|
5
|
+
const KEY_NAME = 'CURSOR_API_KEY';
|
|
6
|
+
/** Translate unified dispatch options into Cursor's documented v1 create shape. */
|
|
7
|
+
export function buildCursorCreateBody(options) {
|
|
8
|
+
const repos = resolveDispatchRepos(options);
|
|
9
|
+
const body = { prompt: { text: options.prompt } };
|
|
10
|
+
if (options.images?.length) {
|
|
11
|
+
body.prompt.images = options.images.map(({ data, mimeType }) => ({ data, mimeType }));
|
|
12
|
+
}
|
|
13
|
+
if (options.model)
|
|
14
|
+
body.model = { id: options.model };
|
|
15
|
+
if (repos.length) {
|
|
16
|
+
body.repos = repos.map((url) => ({ url, ...(options.branch ? { startingRef: options.branch } : {}) }));
|
|
17
|
+
body.autoCreatePR = true;
|
|
18
|
+
}
|
|
19
|
+
if (options.env && Object.keys(options.env).length)
|
|
20
|
+
body.envVars = options.env;
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
/** Map one Cursor agent/run pair into the provider-neutral task shape. */
|
|
24
|
+
export function parseCursorTask(agent, run, prompt = '') {
|
|
25
|
+
const branch = run.git?.branches?.[0];
|
|
26
|
+
return {
|
|
27
|
+
id: agent.id,
|
|
28
|
+
provider: 'cursor',
|
|
29
|
+
status: normalizeProviderStatus('cursor', run.status),
|
|
30
|
+
agent: 'cursor',
|
|
31
|
+
prompt: prompt || agent.name || '',
|
|
32
|
+
repo: agent.repos?.[0]?.url,
|
|
33
|
+
repos: agent.repos?.map((repo) => repo.url),
|
|
34
|
+
branch: branch?.branch,
|
|
35
|
+
prUrl: branch?.prUrl,
|
|
36
|
+
createdAt: agent.createdAt,
|
|
37
|
+
updatedAt: run.updatedAt,
|
|
38
|
+
summary: run.result,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Parse one complete SSE frame into a shared cloud event. */
|
|
42
|
+
export function parseCursorSseFrame(frame) {
|
|
43
|
+
let eventName = 'message';
|
|
44
|
+
const data = [];
|
|
45
|
+
for (const line of frame.split(/\r?\n/)) {
|
|
46
|
+
if (line.startsWith('event:'))
|
|
47
|
+
eventName = line.slice(6).trim();
|
|
48
|
+
else if (line.startsWith('data:'))
|
|
49
|
+
data.push(line.slice(5).trimStart());
|
|
50
|
+
}
|
|
51
|
+
if (!data.length)
|
|
52
|
+
return undefined;
|
|
53
|
+
const raw = data.join('\n');
|
|
54
|
+
let value = raw;
|
|
55
|
+
try {
|
|
56
|
+
value = JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch { /* Cursor may send plain text. */ }
|
|
59
|
+
const obj = typeof value === 'object' && value !== null ? value : {};
|
|
60
|
+
const timestamp = typeof obj.timestamp === 'string' ? obj.timestamp : undefined;
|
|
61
|
+
if (eventName === 'status') {
|
|
62
|
+
const status = String(obj.status ?? obj.data ?? raw);
|
|
63
|
+
return { type: 'status', status: normalizeProviderStatus('cursor', status), timestamp };
|
|
64
|
+
}
|
|
65
|
+
if (eventName === 'assistant') {
|
|
66
|
+
return { type: 'text', content: String(obj.text ?? obj.content ?? obj.data ?? raw), timestamp };
|
|
67
|
+
}
|
|
68
|
+
if (eventName === 'thinking') {
|
|
69
|
+
return { type: 'thinking', content: String(obj.text ?? obj.content ?? obj.data ?? raw), timestamp };
|
|
70
|
+
}
|
|
71
|
+
if (eventName === 'tool_call') {
|
|
72
|
+
if (obj.status === 'completed') {
|
|
73
|
+
return { type: 'tool_result', tool: String(obj.name ?? 'tool'), output: obj.result ?? obj, timestamp };
|
|
74
|
+
}
|
|
75
|
+
return { type: 'tool_use', tool: String(obj.name ?? 'tool'), input: obj.args ?? obj, timestamp };
|
|
76
|
+
}
|
|
77
|
+
if (eventName === 'result' || eventName === 'done') {
|
|
78
|
+
const status = normalizeProviderStatus('cursor', String(obj.status ?? 'FINISHED'));
|
|
79
|
+
return { type: 'done', status, summary: typeof obj.text === 'string' ? obj.text : undefined, timestamp };
|
|
80
|
+
}
|
|
81
|
+
if (eventName === 'error') {
|
|
82
|
+
return { type: 'error', message: String(obj.message ?? raw), timestamp };
|
|
83
|
+
}
|
|
84
|
+
return { type: 'unknown', name: eventName, data: raw, timestamp };
|
|
85
|
+
}
|
|
86
|
+
/** Turn Cursor's structured API errors into actionable provider errors. */
|
|
87
|
+
export function cursorApiError(action, status, text) {
|
|
88
|
+
let parsed = {};
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse(text);
|
|
91
|
+
}
|
|
92
|
+
catch { /* preserve raw body */ }
|
|
93
|
+
const detail = typeof parsed.error === 'object' ? parsed.error : undefined;
|
|
94
|
+
const code = detail?.code;
|
|
95
|
+
const message = detail?.message ?? (typeof parsed.error === 'string' ? parsed.error : text.slice(0, 500));
|
|
96
|
+
if (code === 'plan_required') {
|
|
97
|
+
return new Error(`Cursor Cloud Agents requires a paid Cursor plan; this API key belongs to a plan without Cloud Agents access. ${message}`);
|
|
98
|
+
}
|
|
99
|
+
if (status === 401 || code === 'unauthorized' || code === 'api_key_not_found') {
|
|
100
|
+
return new Error(`Cursor Cloud authentication failed (${status}${code ? ` ${code}` : ''}). Check CURSOR_API_KEY in the configured agents secrets bundle. ${message}`);
|
|
101
|
+
}
|
|
102
|
+
return new Error(`Cursor Cloud ${action} failed (${status}${code ? ` ${code}` : ''}): ${message}`);
|
|
103
|
+
}
|
|
104
|
+
export class CursorCloudProvider {
|
|
105
|
+
id = 'cursor';
|
|
106
|
+
name = 'Cursor Cloud Agents';
|
|
107
|
+
secretsBundle;
|
|
108
|
+
constructor(config) {
|
|
109
|
+
this.secretsBundle = config?.secretsBundle;
|
|
110
|
+
}
|
|
111
|
+
hasKeySource() {
|
|
112
|
+
return Boolean(this.secretsBundle || process.env[KEY_NAME]);
|
|
113
|
+
}
|
|
114
|
+
resolveApiKey() {
|
|
115
|
+
if (this.secretsBundle) {
|
|
116
|
+
try {
|
|
117
|
+
const { env } = readAndResolveBundleEnv(this.secretsBundle, { caller: 'cloud:cursor', agentOnly: true });
|
|
118
|
+
if (env[KEY_NAME])
|
|
119
|
+
return env[KEY_NAME];
|
|
120
|
+
throw new Error(`Secrets bundle '${this.secretsBundle}' has no ${KEY_NAME}. Add one: agents secrets add ${this.secretsBundle} ${KEY_NAME}`);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
throw new Error(`Could not read Cursor API key from bundle '${this.secretsBundle}': ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (process.env[KEY_NAME])
|
|
127
|
+
return process.env[KEY_NAME];
|
|
128
|
+
throw new Error(`Cursor cloud needs an API key. Set cloud.providers.cursor.secretsBundle in ~/.agents/agents.yaml after running: agents secrets add cursor ${KEY_NAME}`);
|
|
129
|
+
}
|
|
130
|
+
capabilities() {
|
|
131
|
+
const available = this.hasKeySource();
|
|
132
|
+
return {
|
|
133
|
+
available,
|
|
134
|
+
dispatch: available,
|
|
135
|
+
status: available,
|
|
136
|
+
list: available,
|
|
137
|
+
stream: available,
|
|
138
|
+
cancel: available,
|
|
139
|
+
message: available,
|
|
140
|
+
multiRepo: true,
|
|
141
|
+
skills: false,
|
|
142
|
+
images: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async request(path, init) {
|
|
146
|
+
const apiKey = this.resolveApiKey();
|
|
147
|
+
let response;
|
|
148
|
+
try {
|
|
149
|
+
response = await fetch(`${CURSOR_API_BASE_URL}${path}`, {
|
|
150
|
+
...init,
|
|
151
|
+
headers: {
|
|
152
|
+
authorization: `Basic ${Buffer.from(`${apiKey}:`).toString('base64')}`,
|
|
153
|
+
...(init?.body ? { 'content-type': 'application/json' } : {}),
|
|
154
|
+
...init?.headers,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
throw new Error(`Cursor Cloud request failed (network): ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
if (!response.ok)
|
|
162
|
+
throw cursorApiError(init?.method ?? 'request', response.status, await response.text());
|
|
163
|
+
return response;
|
|
164
|
+
}
|
|
165
|
+
async latestRun(agent) {
|
|
166
|
+
if (!agent.latestRunId)
|
|
167
|
+
throw new Error(`Cursor Cloud agent ${agent.id} has no runs.`);
|
|
168
|
+
return this.request(`/agents/${encodeURIComponent(agent.id)}/runs/${encodeURIComponent(agent.latestRunId)}`)
|
|
169
|
+
.then((response) => response.json());
|
|
170
|
+
}
|
|
171
|
+
async dispatch(options) {
|
|
172
|
+
const response = await this.request('/agents', { method: 'POST', body: JSON.stringify(buildCursorCreateBody(options)) });
|
|
173
|
+
const payload = await response.json();
|
|
174
|
+
return parseCursorTask(payload.agent, payload.run, options.prompt);
|
|
175
|
+
}
|
|
176
|
+
async status(taskId) {
|
|
177
|
+
const agent = await this.request(`/agents/${encodeURIComponent(taskId)}`).then((response) => response.json());
|
|
178
|
+
return parseCursorTask(agent, await this.latestRun(agent));
|
|
179
|
+
}
|
|
180
|
+
async list(filter) {
|
|
181
|
+
const { items } = await this.request('/agents?limit=100').then((response) => response.json());
|
|
182
|
+
const tasks = await Promise.all(items.filter((agent) => agent.latestRunId).map(async (agent) => parseCursorTask(agent, await this.latestRun(agent))));
|
|
183
|
+
return filter?.status ? tasks.filter((task) => task.status === filter.status) : tasks;
|
|
184
|
+
}
|
|
185
|
+
async *stream(taskId) {
|
|
186
|
+
const agent = await this.request(`/agents/${encodeURIComponent(taskId)}`).then((response) => response.json());
|
|
187
|
+
if (!agent.latestRunId)
|
|
188
|
+
throw new Error(`Cursor Cloud agent ${taskId} has no run to stream.`);
|
|
189
|
+
const response = await this.request(`/agents/${encodeURIComponent(taskId)}/runs/${encodeURIComponent(agent.latestRunId)}/stream`, {
|
|
190
|
+
headers: { accept: 'text/event-stream' },
|
|
191
|
+
});
|
|
192
|
+
if (!response.body)
|
|
193
|
+
throw new Error(`Cursor Cloud stream for ${taskId} returned no response body.`);
|
|
194
|
+
const reader = response.body.getReader();
|
|
195
|
+
const decoder = new TextDecoder();
|
|
196
|
+
let buffer = '';
|
|
197
|
+
while (true) {
|
|
198
|
+
const { done, value } = await reader.read();
|
|
199
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
200
|
+
const frames = buffer.split(/\r?\n\r?\n/);
|
|
201
|
+
buffer = frames.pop() ?? '';
|
|
202
|
+
for (const frame of frames) {
|
|
203
|
+
const event = parseCursorSseFrame(frame);
|
|
204
|
+
if (event)
|
|
205
|
+
yield event;
|
|
206
|
+
}
|
|
207
|
+
if (done)
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (buffer.trim()) {
|
|
211
|
+
const event = parseCursorSseFrame(buffer);
|
|
212
|
+
if (event)
|
|
213
|
+
yield event;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async cancel(taskId) {
|
|
217
|
+
const agent = await this.request(`/agents/${encodeURIComponent(taskId)}`).then((response) => response.json());
|
|
218
|
+
if (!agent.latestRunId)
|
|
219
|
+
throw new Error(`Cursor Cloud agent ${taskId} has no run to cancel.`);
|
|
220
|
+
await this.request(`/agents/${encodeURIComponent(taskId)}/runs/${encodeURIComponent(agent.latestRunId)}/cancel`, { method: 'POST' });
|
|
221
|
+
}
|
|
222
|
+
async message(taskId, content) {
|
|
223
|
+
await this.request(`/agents/${encodeURIComponent(taskId)}/runs`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
body: JSON.stringify({ prompt: { text: content } }),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -11,6 +11,7 @@ import { RushCloudProvider } from './rush.js';
|
|
|
11
11
|
import { CodexCloudProvider } from './codex.js';
|
|
12
12
|
import { FactoryCloudProvider } from './factory.js';
|
|
13
13
|
import { AntigravityCloudProvider } from './antigravity.js';
|
|
14
|
+
import { CursorCloudProvider } from './cursor.js';
|
|
14
15
|
import { HostCloudProvider } from './host.js';
|
|
15
16
|
import { getUserAgentsDir } from '../state.js';
|
|
16
17
|
import { AGENTS } from '../agents.js';
|
|
@@ -44,6 +45,7 @@ function initProviders() {
|
|
|
44
45
|
providers.set('codex', new CodexCloudProvider(config.providers?.codex));
|
|
45
46
|
providers.set('factory', new FactoryCloudProvider(config.providers?.factory));
|
|
46
47
|
providers.set('antigravity', new AntigravityCloudProvider(config.providers?.antigravity));
|
|
48
|
+
providers.set('cursor', new CursorCloudProvider(config.providers?.cursor));
|
|
47
49
|
// Your own machines (agents hosts / agents devices) over SSH. No agent
|
|
48
50
|
// auto-routes here — it's always an explicit --provider host / --host choice.
|
|
49
51
|
providers.set('host', new HostCloudProvider());
|
|
@@ -14,13 +14,14 @@ import type { JobTrigger } from '../routines.js';
|
|
|
14
14
|
* - `codex` — OpenAI Codex Cloud (`codex cloud exec`)
|
|
15
15
|
* - `factory` — Factory Droid Computers (`droid computer ssh` + remote `droid exec`)
|
|
16
16
|
* - `antigravity` — Google Gemini Managed Agents (Interactions API)
|
|
17
|
+
* - `cursor` — Cursor Cloud Agents REST API
|
|
17
18
|
* - `host` — a machine you own (`agents hosts` / `agents devices`), over SSH
|
|
18
19
|
*
|
|
19
20
|
* Agents route to their native cloud automatically (see `cloudProvider` in the
|
|
20
21
|
* agent registry); `--provider` overrides. Nothing auto-routes to `host` — it
|
|
21
22
|
* is always an explicit `--provider host` (or `--host <name>`) choice.
|
|
22
23
|
*/
|
|
23
|
-
export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity' | 'host';
|
|
24
|
+
export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity' | 'cursor' | 'host';
|
|
24
25
|
/**
|
|
25
26
|
* Lifecycle state of a cloud-dispatched task.
|
|
26
27
|
*
|
|
@@ -30,7 +31,7 @@ export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity' | 'ho
|
|
|
30
31
|
*/
|
|
31
32
|
export type CloudTaskStatus = 'queued' | 'allocating' | 'running' | 'idle' | 'input_required' | 'completed' | 'failed' | 'cancelled';
|
|
32
33
|
/** Cloud backends whose wire status is normalized by `normalizeProviderStatus`. */
|
|
33
|
-
export type StatusNormalizingProvider = 'rush' | 'codex' | 'antigravity';
|
|
34
|
+
export type StatusNormalizingProvider = 'rush' | 'codex' | 'antigravity' | 'cursor';
|
|
34
35
|
/**
|
|
35
36
|
* Normalize a provider's raw wire status into the canonical `CloudTaskStatus`.
|
|
36
37
|
*
|
|
@@ -302,6 +303,10 @@ export interface CloudProviderConfig {
|
|
|
302
303
|
secretsBundle?: string;
|
|
303
304
|
model?: string;
|
|
304
305
|
};
|
|
306
|
+
/** Cursor Cloud Agents API. The API key remains in the named secrets bundle. */
|
|
307
|
+
cursor?: {
|
|
308
|
+
secretsBundle?: string;
|
|
309
|
+
};
|
|
305
310
|
}
|
|
306
311
|
/** Top-level `cloud` section of agents.yaml. */
|
|
307
312
|
export interface CloudConfig {
|
package/dist/lib/cloud/types.js
CHANGED
|
@@ -31,6 +31,20 @@ export function normalizeProviderStatus(provider, wireStatus) {
|
|
|
31
31
|
return normalizeCodexStatus(wireStatus ?? '');
|
|
32
32
|
case 'antigravity':
|
|
33
33
|
return normalizeAntigravityStatus(wireStatus);
|
|
34
|
+
case 'cursor':
|
|
35
|
+
return normalizeCursorStatus(wireStatus ?? '');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Cursor Cloud run status → canonical enum. Unknown non-terminal states remain running. */
|
|
39
|
+
function normalizeCursorStatus(s) {
|
|
40
|
+
switch (s.toUpperCase()) {
|
|
41
|
+
case 'CREATING': return 'queued';
|
|
42
|
+
case 'RUNNING': return 'running';
|
|
43
|
+
case 'FINISHED': return 'completed';
|
|
44
|
+
case 'ERROR':
|
|
45
|
+
case 'EXPIRED': return 'failed';
|
|
46
|
+
case 'CANCELLED': return 'cancelled';
|
|
47
|
+
default: return 'running';
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
/** Rush Factory Floor status → canonical enum. Default `running`; no `queued`. */
|
|
@@ -124,8 +124,8 @@ export declare function crabboxFind(slug: string, opts?: CrabboxOptions): Crabbo
|
|
|
124
124
|
export declare function crabboxStatusReady(slug: string, opts?: CrabboxOptions): boolean;
|
|
125
125
|
export interface PoolMatchOptions {
|
|
126
126
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
127
|
+
* Lease pool label (shared default, or `.crabbox.yaml leaseProfile:` opt-in;
|
|
128
|
+
* see config.ts). Both sides normalize an unset profile to
|
|
129
129
|
* DEFAULT_CRABBOX_PROFILE, so profile-less runs match unlabeled boxes.
|
|
130
130
|
*/
|
|
131
131
|
profile?: string;
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
* The repo-local crabbox config (`.crabbox.yaml` at the repo root).
|
|
3
3
|
*
|
|
4
4
|
* crabbox itself reads this file when warming a box (the `profile:` key becomes
|
|
5
|
-
* the box's `profile` label). `agents run --lease`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `pick_ready_box` resolves the pool the same way).
|
|
5
|
+
* the box's `profile` label). `agents run --lease` deliberately does NOT inherit
|
|
6
|
+
* that repo/CI label: leases share the default pool unless `leaseProfile:` opts
|
|
7
|
+
* the repo into a dedicated hot box.
|
|
9
8
|
*/
|
|
10
9
|
/**
|
|
11
10
|
* The profile label a box warm pool shares. Matches sandbox.sh's
|
|
@@ -14,8 +13,8 @@
|
|
|
14
13
|
*/
|
|
15
14
|
export declare const DEFAULT_CRABBOX_PROFILE = "default";
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Pool label for `agents run --lease`. The generic `profile:` key remains owned
|
|
17
|
+
* by repo sandbox/CI scripts; only the explicit `leaseProfile:` key opts lease
|
|
18
|
+
* runs out of the shared default pool.
|
|
20
19
|
*/
|
|
21
|
-
export declare function
|
|
20
|
+
export declare function readCrabboxLeaseProfile(repoRoot: string): string;
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
* The repo-local crabbox config (`.crabbox.yaml` at the repo root).
|
|
3
3
|
*
|
|
4
4
|
* crabbox itself reads this file when warming a box (the `profile:` key becomes
|
|
5
|
-
* the box's `profile` label). `agents run --lease`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `pick_ready_box` resolves the pool the same way).
|
|
5
|
+
* the box's `profile` label). `agents run --lease` deliberately does NOT inherit
|
|
6
|
+
* that repo/CI label: leases share the default pool unless `leaseProfile:` opts
|
|
7
|
+
* the repo into a dedicated hot box.
|
|
9
8
|
*/
|
|
10
9
|
import * as fs from 'fs';
|
|
11
10
|
import * as path from 'path';
|
|
@@ -16,12 +15,7 @@ import * as yaml from 'yaml';
|
|
|
16
15
|
* with no `profile` label both normalize here, so they still match each other.
|
|
17
16
|
*/
|
|
18
17
|
export const DEFAULT_CRABBOX_PROFILE = 'default';
|
|
19
|
-
|
|
20
|
-
* The `profile:` declared by `<repoRoot>/.crabbox.yaml`, or undefined when the
|
|
21
|
-
* file is missing, unreadable, or has no profile key (crabbox then applies its
|
|
22
|
-
* own default, which the pool matcher treats as DEFAULT_CRABBOX_PROFILE).
|
|
23
|
-
*/
|
|
24
|
-
export function readCrabboxRepoProfile(repoRoot) {
|
|
18
|
+
function readCrabboxRepoConfig(repoRoot) {
|
|
25
19
|
let raw;
|
|
26
20
|
try {
|
|
27
21
|
raw = fs.readFileSync(path.join(repoRoot, '.crabbox.yaml'), 'utf-8');
|
|
@@ -36,8 +30,14 @@ export function readCrabboxRepoProfile(repoRoot) {
|
|
|
36
30
|
catch {
|
|
37
31
|
return undefined;
|
|
38
32
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
return parsed && typeof parsed === 'object' ? parsed : undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pool label for `agents run --lease`. The generic `profile:` key remains owned
|
|
37
|
+
* by repo sandbox/CI scripts; only the explicit `leaseProfile:` key opts lease
|
|
38
|
+
* runs out of the shared default pool.
|
|
39
|
+
*/
|
|
40
|
+
export function readCrabboxLeaseProfile(repoRoot) {
|
|
41
|
+
const profile = readCrabboxRepoConfig(repoRoot)?.leaseProfile;
|
|
42
|
+
return typeof profile === 'string' && profile.length > 0 ? profile : DEFAULT_CRABBOX_PROFILE;
|
|
43
43
|
}
|