@cursor/july 0.1.100 → 0.1.101
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/bin/agent-serve.js +3 -2
- package/dist/channels/deployments/deployments-channel.d.ts +41 -5
- package/dist/channels/deployments/deployments-channel.d.ts.map +1 -1
- package/dist/channels/deployments/deployments-channel.js +88 -8
- package/dist/channels/deployments/types.d.ts +4 -3
- package/dist/channels/deployments/types.d.ts.map +1 -1
- package/dist/channels/origin/defaults.d.ts +43 -0
- package/dist/channels/origin/defaults.d.ts.map +1 -0
- package/dist/channels/origin/defaults.js +111 -0
- package/dist/channels/origin/index.d.ts +2 -0
- package/dist/channels/origin/index.d.ts.map +1 -1
- package/dist/channels/origin/index.js +2 -0
- package/dist/channels/origin/origin-channel.d.ts +2 -1
- package/dist/channels/origin/origin-channel.d.ts.map +1 -1
- package/dist/channels/origin/origin-channel.js +24 -3
- package/dist/channels/origin/state.d.ts +32 -0
- package/dist/channels/origin/state.d.ts.map +1 -0
- package/dist/channels/origin/state.js +23 -0
- package/dist/channels/origin/types.d.ts +23 -1
- package/dist/channels/origin/types.d.ts.map +1 -1
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +2 -1
- package/dist/channels/slack/code-fences.d.ts +17 -0
- package/dist/channels/slack/code-fences.d.ts.map +1 -0
- package/dist/channels/slack/code-fences.js +168 -0
- package/dist/channels/slack/index.d.ts +1 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +1 -0
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +11 -4
- package/dist/connections.d.ts +18 -2
- package/dist/connections.d.ts.map +1 -1
- package/dist/connections.js +18 -2
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.DZ1e0Ycq.js → app.BASO9Kih.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DbFXINq2.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.l8omwc6D.js → VPLocalSearchBox.DwkqxoE0.js} +1 -1
- package/dist/docs/assets/chunks/{theme.JVcD6gel.js → theme.CCil_13e.js} +2 -2
- package/dist/docs/assets/{reference_connections.md.BiGoBAk2.js → reference_connections.md.Di6jJAXF.js} +12 -2
- package/dist/docs/assets/{reference_connections.md.BiGoBAk2.lean.js → reference_connections.md.Di6jJAXF.lean.js} +1 -1
- package/dist/docs/assets/{reference_http-api.md.BEJx9XVj.js → reference_http-api.md.oySXBO8o.js} +1 -1
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +4 -4
- package/dist/docs/evals.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/convert-automation.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/mcp-oauth.html +4 -4
- package/dist/docs/guides/opentelemetry.html +4 -4
- package/dist/docs/guides/slack.html +4 -4
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +4 -4
- package/dist/docs/llms-full.txt +32 -1
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/artifacts.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +4 -4
- package/dist/docs/reference/connections.html +16 -6
- package/dist/docs/reference/connections.md +31 -0
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +6 -6
- package/dist/docs/reference/http-api.md +1 -1
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/prompt.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +4 -4
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +4 -4
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/skills/ab.html +4 -4
- package/dist/docs/skills/create-agent.html +4 -4
- package/dist/docs/skills/debug.html +4 -4
- package/dist/docs/skills/evals.html +4 -4
- package/dist/docs/skills/framework-map.html +4 -4
- package/dist/docs/skills/github.html +4 -4
- package/dist/docs/skills/hillclimb.html +4 -4
- package/dist/docs/skills/index.html +4 -4
- package/dist/docs/skills/mcp-auth.html +4 -4
- package/dist/docs/skills/otel.html +4 -4
- package/dist/docs/skills/setup-slack.html +4 -4
- package/dist/docs/storage.html +4 -4
- package/dist/docs/templates/agentic-owners.html +4 -4
- package/dist/docs/templates/agents-md.html +4 -4
- package/dist/docs/templates/code-wiki.html +4 -4
- package/dist/docs/templates/demo.html +4 -4
- package/dist/docs/templates/pr-autofixer.html +4 -4
- package/dist/docs/templates/security-help.html +4 -4
- package/dist/docs/templates/security-reviewer.html +4 -4
- package/dist/docs/templates/triage.html +4 -4
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/extensions/cursor-cloud-agents/extension.d.ts +55 -0
- package/dist/extensions/cursor-cloud-agents/extension.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/extension.js +64 -0
- package/dist/extensions/cursor-cloud-agents/lib/cloud.d.ts +30 -0
- package/dist/extensions/cursor-cloud-agents/lib/cloud.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/lib/cloud.js +45 -0
- package/dist/extensions/cursor-cloud-agents/lib/fleet.d.ts +25 -0
- package/dist/extensions/cursor-cloud-agents/lib/fleet.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/lib/fleet.js +85 -0
- package/dist/extensions/cursor-cloud-agents/lib/repos.d.ts +15 -0
- package/dist/extensions/cursor-cloud-agents/lib/repos.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/lib/repos.js +55 -0
- package/dist/extensions/cursor-cloud-agents/skills/handoff.md +27 -0
- package/dist/extensions/cursor-cloud-agents/tools/follow_up.d.ts +4 -0
- package/dist/extensions/cursor-cloud-agents/tools/follow_up.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/tools/follow_up.js +37 -0
- package/dist/extensions/cursor-cloud-agents/tools/launch.d.ts +4 -0
- package/dist/extensions/cursor-cloud-agents/tools/launch.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/tools/launch.js +83 -0
- package/dist/extensions/cursor-cloud-agents/tools/list.d.ts +4 -0
- package/dist/extensions/cursor-cloud-agents/tools/list.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/tools/list.js +36 -0
- package/dist/extensions/cursor-cloud-agents/tools/status.d.ts +4 -0
- package/dist/extensions/cursor-cloud-agents/tools/status.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/tools/status.js +41 -0
- package/dist/extensions.d.ts +49 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +75 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/internal/advertise-tools.d.ts +4 -1
- package/dist/internal/advertise-tools.d.ts.map +1 -1
- package/dist/internal/advertise-tools.js +20 -1
- package/dist/internal/as-of.d.ts +18 -0
- package/dist/internal/as-of.d.ts.map +1 -1
- package/dist/internal/as-of.js +131 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +64 -19
- package/dist/internal/continuation-identity.d.ts.map +1 -1
- package/dist/internal/continuation-identity.js +3 -1
- package/dist/internal/cursor/hosted-deployment-secrets.d.ts +16 -0
- package/dist/internal/cursor/hosted-deployment-secrets.d.ts.map +1 -0
- package/dist/internal/cursor/hosted-deployment-secrets.js +61 -0
- package/dist/internal/cursor-deployment-relay.d.ts +15 -1
- package/dist/internal/cursor-deployment-relay.d.ts.map +1 -1
- package/dist/internal/cursor-deployment-relay.js +46 -7
- package/dist/internal/deploy-client.d.ts +23 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +54 -12
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +415 -120
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +5 -0
- package/dist/internal/framework-file-storage.d.ts +3 -3
- package/dist/internal/framework-file-storage.d.ts.map +1 -1
- package/dist/internal/framework-file-storage.js +34 -5
- package/dist/internal/hosted-delivery.d.ts +3 -1
- package/dist/internal/hosted-delivery.d.ts.map +1 -1
- package/dist/internal/hosted-delivery.js +19 -32
- package/dist/internal/hosted-execution-diag.d.ts +5 -0
- package/dist/internal/hosted-execution-diag.d.ts.map +1 -1
- package/dist/internal/hosted-execution-diag.js +5 -0
- package/dist/internal/hosted-execution-flush.d.ts +46 -0
- package/dist/internal/hosted-execution-flush.d.ts.map +1 -0
- package/dist/internal/hosted-execution-flush.js +81 -0
- package/dist/internal/mcp-endpoint.js +1 -0
- package/dist/internal/mcp-host.d.ts +2 -1
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +35 -15
- package/dist/internal/review-comments.d.ts +1 -1
- package/dist/internal/review-comments.d.ts.map +1 -1
- package/dist/internal/review-comments.js +4 -4
- package/dist/internal/server.d.ts +3 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +79 -104
- package/dist/internal/session-engine.d.ts +9 -7
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +45 -34
- package/dist/internal/session-run-log.d.ts +2 -1
- package/dist/internal/session-run-log.d.ts.map +1 -1
- package/dist/internal/session-run-log.js +2 -1
- package/dist/internal/start-log-store.d.ts +60 -0
- package/dist/internal/start-log-store.d.ts.map +1 -0
- package/dist/internal/start-log-store.js +151 -0
- package/dist/internal/tool-policy.d.ts +8 -0
- package/dist/internal/tool-policy.d.ts.map +1 -1
- package/dist/internal/tool-policy.js +19 -7
- package/dist/playground/assets/{index-DVs98vPL.js → index-BnNN1-EK.js} +45 -45
- package/dist/playground/assets/index-DBQCwFp0.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-protocol.d.ts +24 -0
- package/dist/storage-protocol.d.ts.map +1 -1
- package/dist/storage-protocol.js +33 -0
- package/dist/storage.d.ts +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1 -1
- package/dist/types.d.ts +118 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +29 -1
- package/docs/reference/connections.md +31 -0
- package/docs/reference/http-api.md +1 -1
- package/package.json +18 -4
- package/src/bin/agent-serve.ts +11 -2
- package/src/channels/deployments/deployments-channel.ts +117 -8
- package/src/channels/deployments/types.ts +4 -3
- package/src/channels/origin/defaults.ts +143 -0
- package/src/channels/origin/index.ts +2 -0
- package/src/channels/origin/origin-channel.ts +37 -1
- package/src/channels/origin/state.ts +54 -0
- package/src/channels/origin/types.ts +24 -0
- package/src/channels/slack/api.ts +2 -1
- package/src/channels/slack/code-fences.ts +183 -0
- package/src/channels/slack/index.ts +1 -0
- package/src/channels/slack/live-delivery.ts +16 -13
- package/src/connections.ts +18 -1
- package/src/extensions/cursor-cloud-agents/extension.ts +118 -0
- package/src/extensions/cursor-cloud-agents/lib/cloud.ts +79 -0
- package/src/extensions/cursor-cloud-agents/lib/fleet.ts +109 -0
- package/src/extensions/cursor-cloud-agents/lib/repos.ts +81 -0
- package/src/extensions/cursor-cloud-agents/skills/handoff.md +27 -0
- package/src/extensions/cursor-cloud-agents/tools/follow_up.ts +29 -0
- package/src/extensions/cursor-cloud-agents/tools/launch.ts +84 -0
- package/src/extensions/cursor-cloud-agents/tools/list.ts +31 -0
- package/src/extensions/cursor-cloud-agents/tools/status.ts +39 -0
- package/src/extensions.ts +98 -0
- package/src/index.ts +2 -0
- package/src/internal/advertise-tools.ts +37 -12
- package/src/internal/as-of.ts +163 -0
- package/src/internal/cli-deploy.ts +86 -23
- package/src/internal/continuation-identity.ts +3 -1
- package/src/internal/cursor/hosted-deployment-secrets.ts +72 -0
- package/src/internal/cursor-deployment-relay.ts +65 -8
- package/src/internal/deploy-client.ts +73 -18
- package/src/internal/discovery.ts +470 -113
- package/src/internal/distribution.ts +5 -0
- package/src/internal/framework-file-storage.ts +39 -5
- package/src/internal/hosted-delivery.ts +38 -63
- package/src/internal/hosted-execution-diag.ts +6 -0
- package/src/internal/hosted-execution-flush.ts +142 -0
- package/src/internal/mcp-endpoint.ts +1 -0
- package/src/internal/mcp-host.ts +40 -14
- package/src/internal/review-comments.ts +5 -2
- package/src/internal/server.ts +81 -100
- package/src/internal/session-engine.ts +56 -28
- package/src/internal/session-run-log.ts +2 -1
- package/src/internal/start-log-store.ts +177 -0
- package/src/internal/tool-policy.ts +39 -9
- package/src/storage-protocol.ts +37 -0
- package/src/storage.ts +1 -0
- package/src/types.ts +172 -14
- package/dist/docs/assets/chunks/@localSearchIndexroot.j70vvPL4.js +0 -1
- package/dist/playground/assets/index-BszoQDc6.css +0 -1
- /package/dist/docs/assets/{reference_http-api.md.BEJx9XVj.lean.js → reference_http-api.md.oySXBO8o.lean.js} +0 -0
|
@@ -4,11 +4,14 @@ import type {
|
|
|
4
4
|
ArtifactsApi,
|
|
5
5
|
AuthContext,
|
|
6
6
|
AuthPolicy,
|
|
7
|
+
ChannelEventHandlers,
|
|
7
8
|
ChannelStartArgs,
|
|
8
9
|
HostContext,
|
|
10
|
+
SendMessageFn,
|
|
9
11
|
} from "../../types.js";
|
|
10
12
|
import type { OriginTokenProvider } from "./api.js";
|
|
11
13
|
import type { OriginHandle } from "./binding.js";
|
|
14
|
+
import type { OriginChannelState } from "./state.js";
|
|
12
15
|
|
|
13
16
|
export interface OriginActor {
|
|
14
17
|
id: string;
|
|
@@ -70,6 +73,12 @@ export interface OriginEventContext {
|
|
|
70
73
|
*/
|
|
71
74
|
payload: Record<string, unknown>;
|
|
72
75
|
sender?: OriginActor;
|
|
76
|
+
/**
|
|
77
|
+
* Channel `send`. Set on every delivery path (HTTP webhook, hosted
|
|
78
|
+
* admission, SCM events) so a hook can start a turn itself instead of
|
|
79
|
+
* returning `{ auth, message }` or a deferred `{ task }`.
|
|
80
|
+
*/
|
|
81
|
+
send?: SendMessageFn;
|
|
73
82
|
host: HostContext;
|
|
74
83
|
artifacts: ArtifactsApi;
|
|
75
84
|
/**
|
|
@@ -242,6 +251,21 @@ export interface OriginChannelOptions {
|
|
|
242
251
|
* pull request URL, or a resolver for custom cloud options.
|
|
243
252
|
*/
|
|
244
253
|
cloud?: boolean | AgentCloudOptions | OriginCloudOptionsResolver;
|
|
254
|
+
/**
|
|
255
|
+
* When false, skip posting completed assistant messages (and failure
|
|
256
|
+
* notices) back to the Origin pull request. Defaults to true — the GitHub
|
|
257
|
+
* channel's reply-delivery posture. Outbound credentials resolve like
|
|
258
|
+
* `ctx.origin`: {@link app} / `ORIGIN_APP_*` env when configured,
|
|
259
|
+
* otherwise the Cursor-account session exchange (hosted v2 deployments
|
|
260
|
+
* post as the deployment service-account principal).
|
|
261
|
+
*/
|
|
262
|
+
deliverReplies?: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Channel stream-event handlers, merged over the built-in reply-delivery
|
|
265
|
+
* defaults (`buildDefaultOriginEvents`) — same override contract as the
|
|
266
|
+
* GitHub channel's `events`.
|
|
267
|
+
*/
|
|
268
|
+
events?: ChannelEventHandlers<OriginChannelState>;
|
|
245
269
|
/** Called once when the server mounts this channel. */
|
|
246
270
|
onStart?: (args: ChannelStartArgs) => void | Promise<void>;
|
|
247
271
|
/** Called when the server shuts down. */
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { ErrorCode, type WebAPIPlatformError, WebClient } from "@slack/web-api";
|
|
6
6
|
import { slackifyMarkdown } from "slackify-markdown";
|
|
7
7
|
import type { ArtifactsApi, HostContext } from "../../types.js";
|
|
8
|
+
import { highlightCodeFences } from "./code-fences.js";
|
|
8
9
|
import {
|
|
9
10
|
SLACK_DEFAULT_LOADING_MESSAGES,
|
|
10
11
|
SLACK_LOADING_MESSAGE_MAX_LEN,
|
|
@@ -495,7 +496,7 @@ export async function postMessage(args: {
|
|
|
495
496
|
operation: "chat.postMessage",
|
|
496
497
|
body: {
|
|
497
498
|
...base,
|
|
498
|
-
markdown_text: args.text,
|
|
499
|
+
markdown_text: highlightCodeFences(args.text),
|
|
499
500
|
},
|
|
500
501
|
});
|
|
501
502
|
// Older workspaces / scopes may not accept markdown_text — retry with text.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack highlights a fenced code block only when its info string is a
|
|
3
|
+
* language it knows (`ts`, `python`, …). Models on this harness cite code
|
|
4
|
+
* with Cursor's `startLine:endLine:path` form, or a bare path, which Slack
|
|
5
|
+
* shows verbatim as the block's header with no highlighting. The header is
|
|
6
|
+
* the only slot Slack offers, so a block gets colors by moving the reference
|
|
7
|
+
* to a comment on its first line. Files whose language has no comment
|
|
8
|
+
* syntax, or that Slack cannot color anyway, keep the reference as header.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const FENCE = /^(\s*)(`{3,}|~{3,})(.*)$/;
|
|
12
|
+
/** A line that may still grow into a fence. */
|
|
13
|
+
const FENCE_START = /^\s*(`{1,2}|~{1,2})$/;
|
|
14
|
+
const CITATION = /^(\d+):(\d+):(\S+)$/;
|
|
15
|
+
|
|
16
|
+
const LANGUAGE_BY_EXTENSION: Readonly<Record<string, string>> = {
|
|
17
|
+
ts: "ts",
|
|
18
|
+
mts: "ts",
|
|
19
|
+
cts: "ts",
|
|
20
|
+
tsx: "tsx",
|
|
21
|
+
js: "js",
|
|
22
|
+
mjs: "js",
|
|
23
|
+
cjs: "js",
|
|
24
|
+
jsx: "jsx",
|
|
25
|
+
py: "python",
|
|
26
|
+
go: "go",
|
|
27
|
+
rs: "rust",
|
|
28
|
+
java: "java",
|
|
29
|
+
kt: "kotlin",
|
|
30
|
+
swift: "swift",
|
|
31
|
+
rb: "ruby",
|
|
32
|
+
php: "php",
|
|
33
|
+
c: "c",
|
|
34
|
+
h: "c",
|
|
35
|
+
cc: "cpp",
|
|
36
|
+
cpp: "cpp",
|
|
37
|
+
hpp: "cpp",
|
|
38
|
+
cs: "csharp",
|
|
39
|
+
json: "json",
|
|
40
|
+
jsonc: "jsonc",
|
|
41
|
+
yaml: "yaml",
|
|
42
|
+
yml: "yaml",
|
|
43
|
+
toml: "toml",
|
|
44
|
+
xml: "xml",
|
|
45
|
+
html: "html",
|
|
46
|
+
css: "css",
|
|
47
|
+
scss: "scss",
|
|
48
|
+
md: "markdown",
|
|
49
|
+
mdx: "mdx",
|
|
50
|
+
sh: "bash",
|
|
51
|
+
bash: "bash",
|
|
52
|
+
zsh: "bash",
|
|
53
|
+
sql: "sql",
|
|
54
|
+
proto: "protobuf",
|
|
55
|
+
tf: "hcl",
|
|
56
|
+
graphql: "graphql",
|
|
57
|
+
dockerfile: "dockerfile",
|
|
58
|
+
ini: "ini",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** `[open, close]` comment delimiters; languages without one (JSON) are absent. */
|
|
62
|
+
const COMMENT_BY_LANGUAGE: Readonly<Record<string, readonly [string, string]>> =
|
|
63
|
+
{
|
|
64
|
+
ts: ["//", ""],
|
|
65
|
+
tsx: ["//", ""],
|
|
66
|
+
js: ["//", ""],
|
|
67
|
+
jsx: ["//", ""],
|
|
68
|
+
go: ["//", ""],
|
|
69
|
+
rust: ["//", ""],
|
|
70
|
+
java: ["//", ""],
|
|
71
|
+
kotlin: ["//", ""],
|
|
72
|
+
swift: ["//", ""],
|
|
73
|
+
php: ["//", ""],
|
|
74
|
+
c: ["//", ""],
|
|
75
|
+
cpp: ["//", ""],
|
|
76
|
+
csharp: ["//", ""],
|
|
77
|
+
jsonc: ["//", ""],
|
|
78
|
+
protobuf: ["//", ""],
|
|
79
|
+
python: ["#", ""],
|
|
80
|
+
ruby: ["#", ""],
|
|
81
|
+
yaml: ["#", ""],
|
|
82
|
+
toml: ["#", ""],
|
|
83
|
+
bash: ["#", ""],
|
|
84
|
+
dockerfile: ["#", ""],
|
|
85
|
+
ini: ["#", ""],
|
|
86
|
+
hcl: ["#", ""],
|
|
87
|
+
graphql: ["#", ""],
|
|
88
|
+
sql: ["--", ""],
|
|
89
|
+
css: ["/*", "*/"],
|
|
90
|
+
scss: ["/*", "*/"],
|
|
91
|
+
html: ["<!--", "-->"],
|
|
92
|
+
xml: ["<!--", "-->"],
|
|
93
|
+
markdown: ["<!--", "-->"],
|
|
94
|
+
mdx: ["<!--", "-->"],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function languageForPath(path: string): string | undefined {
|
|
98
|
+
const base = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
|
|
99
|
+
const ext = base.includes(".") ? base.slice(base.lastIndexOf(".") + 1) : base;
|
|
100
|
+
return LANGUAGE_BY_EXTENSION[ext];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function looksLikePath(info: string): boolean {
|
|
104
|
+
return info.includes("/") || languageForPath(info) !== undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Replacement lines for an opening fence whose info string is a citation or
|
|
109
|
+
* a path: the fence tagged with the language, then the reference as a
|
|
110
|
+
* comment. Falls back to the reference as the header when the block cannot
|
|
111
|
+
* carry a comment. `null` when the info string is neither.
|
|
112
|
+
*/
|
|
113
|
+
function rewriteOpening(
|
|
114
|
+
indent: string,
|
|
115
|
+
marks: string,
|
|
116
|
+
info: string
|
|
117
|
+
): string[] | null {
|
|
118
|
+
const token = info.trim().split(/\s+/)[0] ?? "";
|
|
119
|
+
const cited = CITATION.exec(token);
|
|
120
|
+
const path = cited?.[3] ?? (looksLikePath(token) ? token : undefined);
|
|
121
|
+
if (path === undefined) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const reference = cited === null ? path : `${path}:${cited[1]}-${cited[2]}`;
|
|
125
|
+
const language = languageForPath(path);
|
|
126
|
+
const comment =
|
|
127
|
+
language === undefined ? undefined : COMMENT_BY_LANGUAGE[language];
|
|
128
|
+
if (comment === undefined) {
|
|
129
|
+
return [`${indent}${marks}${reference}`];
|
|
130
|
+
}
|
|
131
|
+
const [open, close] = comment;
|
|
132
|
+
const commented =
|
|
133
|
+
close === "" ? `${open} ${reference}` : `${open} ${reference} ${close}`;
|
|
134
|
+
return [`${indent}${marks}${language}`, `${indent}${commented}`];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Rewrite fence info strings so Slack highlights the block. Safe to run on
|
|
139
|
+
* a growing stream: the output for a prefix is a prefix of the output for
|
|
140
|
+
* the full text, because an opening fence whose line has not ended yet is
|
|
141
|
+
* held back rather than emitted half-formed.
|
|
142
|
+
*/
|
|
143
|
+
export function highlightCodeFences(markdown: string): string {
|
|
144
|
+
const complete = markdown.endsWith("\n");
|
|
145
|
+
const lines = markdown.split("\n");
|
|
146
|
+
if (complete) {
|
|
147
|
+
lines.pop();
|
|
148
|
+
}
|
|
149
|
+
const out: string[] = [];
|
|
150
|
+
let open: { marks: string } | null = null;
|
|
151
|
+
const heldBack = (): string =>
|
|
152
|
+
`${out.join("\n")}${out.length > 0 ? "\n" : ""}`;
|
|
153
|
+
for (const [index, line] of lines.entries()) {
|
|
154
|
+
const last = index === lines.length - 1 && !complete;
|
|
155
|
+
const fence = FENCE.exec(line);
|
|
156
|
+
if (fence === null) {
|
|
157
|
+
if (last && open === null && FENCE_START.test(line)) {
|
|
158
|
+
return heldBack();
|
|
159
|
+
}
|
|
160
|
+
out.push(line);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const [, indent = "", marks = "", info = ""] = fence;
|
|
164
|
+
if (open !== null) {
|
|
165
|
+
if (
|
|
166
|
+
marks[0] === open.marks[0] &&
|
|
167
|
+
marks.length >= open.marks.length &&
|
|
168
|
+
info.trim() === ""
|
|
169
|
+
) {
|
|
170
|
+
open = null;
|
|
171
|
+
}
|
|
172
|
+
out.push(line);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (last) {
|
|
176
|
+
// Opening fence still being written: wait for the whole line.
|
|
177
|
+
return heldBack();
|
|
178
|
+
}
|
|
179
|
+
open = { marks };
|
|
180
|
+
out.push(...(rewriteOpening(indent, marks, info) ?? [line]));
|
|
181
|
+
}
|
|
182
|
+
return complete ? `${out.join("\n")}\n` : out.join("\n");
|
|
183
|
+
}
|
|
@@ -21,6 +21,7 @@ export * from "./auth.js";
|
|
|
21
21
|
export * from "./blocks.js";
|
|
22
22
|
export * from "./bot-mentions.js";
|
|
23
23
|
export * from "./channel-watch.js";
|
|
24
|
+
export * from "./code-fences.js";
|
|
24
25
|
export * from "./constants.js";
|
|
25
26
|
export * from "./credentials.js";
|
|
26
27
|
export * from "./defaults.js";
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
startChatStream,
|
|
23
23
|
stopChatStream,
|
|
24
24
|
} from "./api.js";
|
|
25
|
+
import { highlightCodeFences } from "./code-fences.js";
|
|
25
26
|
import { SLACK_MARKDOWN_TEXT_MAX_LEN } from "./constants.js";
|
|
26
27
|
import type { SlackLog } from "./log.js";
|
|
27
28
|
import {
|
|
@@ -117,7 +118,7 @@ export function createStreamDeliveryStrategy(
|
|
|
117
118
|
const openingText = markdown_text ?? "";
|
|
118
119
|
const opening: StreamChunk[] = [];
|
|
119
120
|
if (openingText !== "") {
|
|
120
|
-
opening.push(markdownChunk(openingText));
|
|
121
|
+
opening.push(markdownChunk(highlightCodeFences(openingText)));
|
|
121
122
|
}
|
|
122
123
|
opening.push(...(chunks ?? []));
|
|
123
124
|
if (opening.length === 0) {
|
|
@@ -181,16 +182,24 @@ export function createStreamDeliveryStrategy(
|
|
|
181
182
|
return opened ? activeStreamTs(args.channel) : null;
|
|
182
183
|
};
|
|
183
184
|
|
|
185
|
+
/**
|
|
186
|
+
* What to append for the new cumulative text, as Slack should see it: the
|
|
187
|
+
* block text on both sides goes through the same fence rewrite, so the
|
|
188
|
+
* delta lines up with what the stream already holds.
|
|
189
|
+
*/
|
|
190
|
+
const streamDelta = (channel: SlackChannel, text: string): string =>
|
|
191
|
+
computeStreamAppend(
|
|
192
|
+
highlightCodeFences(channel.state.streamBlockText ?? ""),
|
|
193
|
+
highlightCodeFences(text)
|
|
194
|
+
);
|
|
195
|
+
|
|
184
196
|
const stopWithSuffix = async (args: {
|
|
185
197
|
channel: SlackChannel;
|
|
186
198
|
channelId: string;
|
|
187
199
|
ts: string;
|
|
188
200
|
text: string;
|
|
189
201
|
}): Promise<{ ok: boolean; error?: string }> => {
|
|
190
|
-
const appendText =
|
|
191
|
-
args.channel.state.streamBlockText,
|
|
192
|
-
args.text
|
|
193
|
-
);
|
|
202
|
+
const appendText = streamDelta(args.channel, args.text);
|
|
194
203
|
const stopped = await stopChatStream({
|
|
195
204
|
botToken,
|
|
196
205
|
channel: args.channelId,
|
|
@@ -299,10 +308,7 @@ export function createStreamDeliveryStrategy(
|
|
|
299
308
|
if (ts === null) {
|
|
300
309
|
return;
|
|
301
310
|
}
|
|
302
|
-
const appendText =
|
|
303
|
-
channel.state.streamBlockText,
|
|
304
|
-
text
|
|
305
|
-
);
|
|
311
|
+
const appendText = streamDelta(channel, text);
|
|
306
312
|
if (appendText === "") {
|
|
307
313
|
return;
|
|
308
314
|
}
|
|
@@ -374,10 +380,7 @@ export function createStreamDeliveryStrategy(
|
|
|
374
380
|
return { delivered: false };
|
|
375
381
|
}
|
|
376
382
|
// Measured before the stop attempt mutates stream state.
|
|
377
|
-
const remainder =
|
|
378
|
-
channel.state.streamBlockText,
|
|
379
|
-
text
|
|
380
|
-
);
|
|
383
|
+
const remainder = streamDelta(channel, text);
|
|
381
384
|
const stopped =
|
|
382
385
|
channel.state.streamFull === true
|
|
383
386
|
? { ok: false, error: "msg_too_long" }
|
package/src/connections.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* are available to the agent (SDK `mcpServers`), host (`ctx.host.mcp`),
|
|
9
9
|
* and channel/schedule handlers. Pass `advertiseTools: true` for named
|
|
10
10
|
* tools on local turns. Pass `effects: "read"` with `advertiseTools` to
|
|
11
|
-
* advertise only tools the server classifies as reads.
|
|
11
|
+
* advertise only tools the server classifies as reads. Pass `tools` as
|
|
12
|
+
* a name list to keep a subset, same shape as `defineAgent({ tools })`.
|
|
13
|
+
* Pass `disallowedTools` to drop names. Files under
|
|
12
14
|
* `host-connections/` stay off the model.
|
|
13
15
|
*
|
|
14
16
|
* ```ts
|
|
@@ -42,6 +44,20 @@
|
|
|
42
44
|
* advertiseTools: true,
|
|
43
45
|
* effects: "read",
|
|
44
46
|
* });
|
|
47
|
+
*
|
|
48
|
+
* // named subset: advertise and call only these tools
|
|
49
|
+
* export default defineConnection({
|
|
50
|
+
* url: "https://mcp.example.com/inventory",
|
|
51
|
+
* advertiseTools: true,
|
|
52
|
+
* tools: ["search_skus", "get_stock"],
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* // drop a few tools from a large server
|
|
56
|
+
* export default defineConnection({
|
|
57
|
+
* url: "https://mcp.example.com/inventory",
|
|
58
|
+
* advertiseTools: true,
|
|
59
|
+
* disallowedTools: ["place_order", "delete_sku"],
|
|
60
|
+
* });
|
|
45
61
|
* ```
|
|
46
62
|
*
|
|
47
63
|
* A **Cursor account MCP connection** exposes the MCP connectors the operator
|
|
@@ -129,6 +145,7 @@ export {
|
|
|
129
145
|
isInlineStdioConnection,
|
|
130
146
|
isToolEffect,
|
|
131
147
|
normalizeConnectionEffects,
|
|
148
|
+
normalizeConnectionTools,
|
|
132
149
|
normalizeCursorAccountServers,
|
|
133
150
|
} from "./types.js";
|
|
134
151
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor cloud agents as an extension. Mount it and the agent can hand
|
|
3
|
+
* coding work to Cursor cloud agents and steer a fleet of them:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* // agent/extensions/cloud.ts
|
|
7
|
+
* import cursorCloudAgents from "@cursor/july/extensions/cursor-cloud-agents";
|
|
8
|
+
*
|
|
9
|
+
* export default cursorCloudAgents({
|
|
10
|
+
* cloud: { repos: [{ url: "https://github.com/acme/api" }], autoCreatePR: true },
|
|
11
|
+
* });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Contributes `cloud__launch`, `cloud__follow_up`, `cloud__status`,
|
|
15
|
+
* `cloud__list` and the `cloud__handoff` skill (the namespace is the mount
|
|
16
|
+
* filename). Calls go through the Cursor SDK with the serve host's Cursor
|
|
17
|
+
* API key (`CURSOR_API_KEY`, key file, or `login`); each launched agent
|
|
18
|
+
* stays bound to the principal that launched it, and launches go to the
|
|
19
|
+
* configured repositories. The tools run on the serve host, so they need
|
|
20
|
+
* the local runtime.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { defineExtension } from "../../extensions.js";
|
|
25
|
+
import type {
|
|
26
|
+
AgentCloudOptions,
|
|
27
|
+
Extension,
|
|
28
|
+
ModelSetting,
|
|
29
|
+
} from "../../types.js";
|
|
30
|
+
|
|
31
|
+
/** Mount options; every field is optional. */
|
|
32
|
+
export interface CursorCloudAgentsOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Launch defaults: target `repos`, `env`, `autoCreatePR`, ... in the same
|
|
35
|
+
* vocabulary as `defineAgent({ cloud })`. Configured `repos` are also the
|
|
36
|
+
* allowlist for a launch's own `repo` / `prUrl`; model-chosen targets are
|
|
37
|
+
* refused when no allowlist is configured. With none configured,
|
|
38
|
+
* `needsApproval` defaults to true so prompt-only launches park first.
|
|
39
|
+
*/
|
|
40
|
+
cloud?: AgentCloudOptions;
|
|
41
|
+
/** Model for launched agents. The account default when omitted. */
|
|
42
|
+
model?: ModelSetting;
|
|
43
|
+
/**
|
|
44
|
+
* Park launches and follow-ups for a human approval. Defaults to `true`
|
|
45
|
+
* when no `cloud.repos` are configured (prompt-only launches still use
|
|
46
|
+
* the host key); `false` once repositories are configured. Pass an
|
|
47
|
+
* explicit boolean to override. Model-chosen `repo` / `prUrl` still
|
|
48
|
+
* require the allowlist regardless of this flag.
|
|
49
|
+
*/
|
|
50
|
+
needsApproval?: boolean;
|
|
51
|
+
/** Cursor API key. Defaults to the serve host's own key. */
|
|
52
|
+
apiKey?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** {@link CursorCloudAgentsOptions} with defaults applied. */
|
|
56
|
+
export interface CursorCloudAgentsConfig {
|
|
57
|
+
cloud: AgentCloudOptions;
|
|
58
|
+
model?: ModelSetting;
|
|
59
|
+
needsApproval: boolean;
|
|
60
|
+
apiKey?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const repo = z.object({
|
|
64
|
+
url: z.string().url(),
|
|
65
|
+
startingRef: z.string().optional(),
|
|
66
|
+
prUrl: z.string().url().optional(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const cloud: z.ZodType<AgentCloudOptions> = z.object({
|
|
70
|
+
env: z
|
|
71
|
+
.discriminatedUnion("type", [
|
|
72
|
+
z.object({ type: z.literal("cloud"), name: z.string().optional() }),
|
|
73
|
+
z.object({ type: z.literal("pool"), name: z.string().optional() }),
|
|
74
|
+
z.object({ type: z.literal("machine"), name: z.string().optional() }),
|
|
75
|
+
])
|
|
76
|
+
.optional(),
|
|
77
|
+
repos: z.array(repo).optional(),
|
|
78
|
+
workOnCurrentBranch: z.boolean().optional(),
|
|
79
|
+
autoCreatePR: z.boolean().optional(),
|
|
80
|
+
skipReviewerRequest: z.boolean().optional(),
|
|
81
|
+
envVars: z.record(z.string()).optional(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const model: z.ZodType<ModelSetting> = z.union([
|
|
85
|
+
z.string().min(1),
|
|
86
|
+
z.object({
|
|
87
|
+
id: z.string().min(1),
|
|
88
|
+
params: z.array(z.object({ id: z.string(), value: z.string() })).optional(),
|
|
89
|
+
}),
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
const config: z.ZodType<
|
|
93
|
+
CursorCloudAgentsConfig,
|
|
94
|
+
z.ZodTypeDef,
|
|
95
|
+
CursorCloudAgentsOptions | undefined
|
|
96
|
+
> = z
|
|
97
|
+
.object({
|
|
98
|
+
cloud: cloud.default({}),
|
|
99
|
+
model: model.optional(),
|
|
100
|
+
needsApproval: z.boolean().optional(),
|
|
101
|
+
apiKey: z.string().optional(),
|
|
102
|
+
})
|
|
103
|
+
.default({})
|
|
104
|
+
.transform((options) => ({
|
|
105
|
+
...options,
|
|
106
|
+
needsApproval: options.needsApproval ?? options.cloud.repos === undefined,
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const cursorCloudAgents: Extension<
|
|
110
|
+
CursorCloudAgentsConfig,
|
|
111
|
+
CursorCloudAgentsOptions | undefined
|
|
112
|
+
> = defineExtension({
|
|
113
|
+
root: import.meta.dirname,
|
|
114
|
+
description: "Launch, steer, and monitor Cursor cloud agents.",
|
|
115
|
+
config,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export default cursorCloudAgents;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** The Cursor SDK surface the tools share: auth, model, result views. */
|
|
2
|
+
|
|
3
|
+
import type { ModelSelection, Run, RunStatus, SDKAgentInfo } from "@cursor/sdk";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import {
|
|
6
|
+
cursorWebsiteUrl,
|
|
7
|
+
resolveApiKeySync,
|
|
8
|
+
SIGN_IN_HINT,
|
|
9
|
+
} from "../../../internal/cursor/credentials.js";
|
|
10
|
+
import { toModelSelection } from "../../../internal/sdk-runner.js";
|
|
11
|
+
import extension from "../extension.js";
|
|
12
|
+
|
|
13
|
+
export const agentRef: z.ZodObject<{ agentId: z.ZodString }> = z.object({
|
|
14
|
+
agentId: z.string().describe("Agent id (`bc-...`) returned by a launch."),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export function apiKey(): string {
|
|
18
|
+
const key = resolveApiKeySync({ apiKey: extension.config.apiKey });
|
|
19
|
+
if (key === undefined) {
|
|
20
|
+
throw new Error(`Cloud agents need a Cursor API key. ${SIGN_IN_HINT}`);
|
|
21
|
+
}
|
|
22
|
+
return key.apiKey;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function modelSelection(): ModelSelection | undefined {
|
|
26
|
+
const { model } = extension.config;
|
|
27
|
+
return model === undefined ? undefined : toModelSelection(model);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function agentUrl(agentId: string): string {
|
|
31
|
+
return `${cursorWebsiteUrl()}/agents/${agentId}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Type aliases, not interfaces: views flow into tool results.
|
|
35
|
+
export type AgentView = {
|
|
36
|
+
agentId: string;
|
|
37
|
+
name: string;
|
|
38
|
+
archived: boolean | undefined;
|
|
39
|
+
url: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type RunView = {
|
|
43
|
+
id: string;
|
|
44
|
+
status: RunStatus;
|
|
45
|
+
result: string | undefined;
|
|
46
|
+
error: string | undefined;
|
|
47
|
+
durationMs: number | undefined;
|
|
48
|
+
branches:
|
|
49
|
+
| Array<{
|
|
50
|
+
repoUrl: string;
|
|
51
|
+
branch: string | undefined;
|
|
52
|
+
prUrl: string | undefined;
|
|
53
|
+
}>
|
|
54
|
+
| undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function agentView(info: SDKAgentInfo): AgentView {
|
|
58
|
+
return {
|
|
59
|
+
agentId: info.agentId,
|
|
60
|
+
name: info.name,
|
|
61
|
+
archived: info.archived,
|
|
62
|
+
url: agentUrl(info.agentId),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function runView(run: Run): RunView {
|
|
67
|
+
return {
|
|
68
|
+
id: run.id,
|
|
69
|
+
status: run.status,
|
|
70
|
+
result: run.result,
|
|
71
|
+
error: run.error?.message,
|
|
72
|
+
durationMs: run.durationMs,
|
|
73
|
+
branches: run.git?.branches.map((branch) => ({
|
|
74
|
+
repoUrl: branch.repoUrl,
|
|
75
|
+
branch: branch.branch,
|
|
76
|
+
prUrl: branch.prUrl,
|
|
77
|
+
})),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who launched which cloud agent. The tools authenticate with the serve
|
|
3
|
+
* host's one Cursor API key, so the host itself must keep every session's
|
|
4
|
+
* cloud agents bound to the principal that launched them: status and
|
|
5
|
+
* follow-ups only reach agents recorded here for the same principal, and
|
|
6
|
+
* the fleet listing is that principal's records, by default narrowed to the
|
|
7
|
+
* session that launched them.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import type { ToolContext } from "../../../types.js";
|
|
12
|
+
|
|
13
|
+
const PREFIX = "cloud-agents";
|
|
14
|
+
|
|
15
|
+
// Type alias, not an interface: fleet entries flow into tool results.
|
|
16
|
+
export type FleetEntry = {
|
|
17
|
+
agentId: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
launchedAt: string;
|
|
20
|
+
/** The session whose turn launched the agent; absent on records older than this field. */
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type FleetScope = "session" | "all";
|
|
25
|
+
|
|
26
|
+
const fleetSchema: z.ZodType<FleetEntry[]> = z.array(
|
|
27
|
+
z.object({
|
|
28
|
+
agentId: z.string(),
|
|
29
|
+
name: z.string().optional(),
|
|
30
|
+
launchedAt: z.string(),
|
|
31
|
+
sessionId: z.string().optional(),
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
const ownerSchema = z.object({ principal: z.string() });
|
|
35
|
+
|
|
36
|
+
// `host.kv` has no compare-and-swap, and a model that launches two agents in
|
|
37
|
+
// one turn calls this tool in parallel. Chain the index read-modify-write per
|
|
38
|
+
// principal so neither launch overwrites the other's entry.
|
|
39
|
+
const indexWrites = new Map<string, Promise<void>>();
|
|
40
|
+
|
|
41
|
+
function withIndexLock(key: string, task: () => Promise<void>): Promise<void> {
|
|
42
|
+
const run = (indexWrites.get(key) ?? Promise.resolve()).then(task, task);
|
|
43
|
+
indexWrites.set(key, run);
|
|
44
|
+
return run.finally(() => {
|
|
45
|
+
if (indexWrites.get(key) === run) {
|
|
46
|
+
indexWrites.delete(key);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Unauthenticated sessions (local dev, direct calls) share one principal. */
|
|
52
|
+
export function principalOf(ctx: ToolContext): string {
|
|
53
|
+
const auth = ctx.session.auth;
|
|
54
|
+
return auth === null
|
|
55
|
+
? "anonymous"
|
|
56
|
+
: `${auth.principalType}:${auth.principalId}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function recordLaunch(
|
|
60
|
+
ctx: ToolContext,
|
|
61
|
+
agentId: string,
|
|
62
|
+
name: string | undefined
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
const principal = principalOf(ctx);
|
|
65
|
+
const entry: FleetEntry = {
|
|
66
|
+
agentId,
|
|
67
|
+
name,
|
|
68
|
+
launchedAt: new Date().toISOString(),
|
|
69
|
+
sessionId: ctx.session.id,
|
|
70
|
+
};
|
|
71
|
+
await ctx.host.kv.put(`${PREFIX}/agents/${agentId}`, { principal });
|
|
72
|
+
await withIndexLock(fleetKey(principal), async () => {
|
|
73
|
+
const fleet = await listFleet(ctx);
|
|
74
|
+
await ctx.host.kv.put(fleetKey(principal), [entry, ...fleet]);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Refuse agents launched by another principal, or not through the extension. */
|
|
79
|
+
export async function assertOwned(
|
|
80
|
+
ctx: ToolContext,
|
|
81
|
+
agentId: string
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
const owner = ownerSchema.safeParse(
|
|
84
|
+
await ctx.host.kv.get(`${PREFIX}/agents/${agentId}`)
|
|
85
|
+
);
|
|
86
|
+
if (!owner.success || owner.data.principal !== principalOf(ctx)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Agent ${agentId} was not launched through this extension by this principal; only your own launches can be read or steered.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The caller's launches, newest first: this session's, or every session's. */
|
|
94
|
+
export async function listFleet(
|
|
95
|
+
ctx: ToolContext,
|
|
96
|
+
scope: FleetScope = "all"
|
|
97
|
+
): Promise<FleetEntry[]> {
|
|
98
|
+
const fleet = fleetSchema.safeParse(
|
|
99
|
+
await ctx.host.kv.get(fleetKey(principalOf(ctx)))
|
|
100
|
+
);
|
|
101
|
+
const entries = fleet.success ? fleet.data : [];
|
|
102
|
+
return scope === "all"
|
|
103
|
+
? entries
|
|
104
|
+
: entries.filter((entry) => entry.sessionId === ctx.session.id);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fleetKey(principal: string): string {
|
|
108
|
+
return `${PREFIX}/principals/${encodeURIComponent(principal)}`;
|
|
109
|
+
}
|