@cursor/july 0.1.73 → 0.1.75
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/AGENTS.md +13 -8
- package/dist/bin/agent-serve.js +7 -5
- package/dist/channels/checks.d.ts +111 -0
- package/dist/channels/checks.d.ts.map +1 -0
- package/dist/channels/checks.js +86 -0
- package/dist/channels/deployments/deployments-channel.d.ts +44 -0
- package/dist/channels/deployments/deployments-channel.d.ts.map +1 -0
- package/dist/channels/deployments/deployments-channel.js +92 -0
- package/dist/channels/deployments/index.d.ts +26 -0
- package/dist/channels/deployments/index.d.ts.map +1 -0
- package/dist/channels/deployments/index.js +25 -0
- package/dist/channels/deployments/types.d.ts +69 -0
- package/dist/channels/deployments/types.d.ts.map +1 -0
- package/dist/channels/deployments/types.js +6 -0
- package/dist/channels/github/checks.d.ts +13 -48
- package/dist/channels/github/checks.d.ts.map +1 -1
- package/dist/channels/github/checks.js +56 -64
- package/dist/channels/origin/api.d.ts +100 -0
- package/dist/channels/origin/api.d.ts.map +1 -0
- package/dist/channels/origin/api.js +225 -0
- package/dist/channels/origin/binding.d.ts +44 -0
- package/dist/channels/origin/binding.d.ts.map +1 -0
- package/dist/channels/origin/binding.js +45 -0
- package/dist/channels/origin/checks.d.ts +107 -0
- package/dist/channels/origin/checks.d.ts.map +1 -0
- package/dist/channels/origin/checks.js +145 -0
- package/dist/channels/origin/index.d.ts +3 -0
- package/dist/channels/origin/index.d.ts.map +1 -1
- package/dist/channels/origin/index.js +3 -0
- package/dist/channels/origin/origin-channel.d.ts +4 -4
- package/dist/channels/origin/origin-channel.d.ts.map +1 -1
- package/dist/channels/origin/origin-channel.js +12 -2
- package/dist/channels/origin/types.d.ts +36 -0
- package/dist/channels/origin/types.d.ts.map +1 -1
- package/dist/channels.d.ts +1 -0
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +1 -0
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.CdbcFD8T.js → app.b8bhB2SV.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DGZeXVqn.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.D_NKImM7.js → VPLocalSearchBox.lLSeIyWw.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CMvz7cu9.js → theme.C_owesa-.js} +2 -2
- package/dist/docs/assets/{reference_channels.md.MN8iDy8V.js → reference_channels.md.G5GysZ7e.js} +2 -2
- package/dist/docs/assets/{reference_channels.md.MN8iDy8V.lean.js → reference_channels.md.G5GysZ7e.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.Cl14Jfo3.js → reference_cli.md.TAaYU8br.js} +3 -3
- package/dist/docs/assets/{reference_cli.md.Cl14Jfo3.lean.js → reference_cli.md.TAaYU8br.lean.js} +1 -1
- package/dist/docs/assets/scaffolding-agents.md.CiGsJ1aw.js +1 -0
- package/dist/docs/assets/{scaffolding-agents.md.CrWSA0Ko.lean.js → scaffolding-agents.md.CiGsJ1aw.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/convert-automation.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +5 -5
- package/dist/docs/reference/cli.html +6 -6
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +3 -3
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/internal/cli-skills.d.ts +4 -2
- package/dist/internal/cli-skills.d.ts.map +1 -1
- package/dist/internal/cli-skills.js +8 -3
- package/dist/internal/cursor-deployment-relay.d.ts +77 -0
- package/dist/internal/cursor-deployment-relay.d.ts.map +1 -0
- package/dist/internal/cursor-deployment-relay.js +201 -0
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/install-cursor-skills.d.ts +71 -16
- package/dist/internal/install-cursor-skills.d.ts.map +1 -1
- package/dist/internal/install-cursor-skills.js +160 -48
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +6 -2
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +114 -5
- package/dist/playground/assets/index-BJy9UJrF.js +85 -0
- package/dist/playground/assets/index-BrSd5vff.css +1 -0
- package/dist/playground/index.html +2 -2
- package/docs/reference/channels.md +15 -0
- package/docs/reference/cli.md +16 -8
- package/docs/scaffolding-agents.md +14 -6
- package/package.json +9 -1
- package/scripts/install-cursor-skills.mjs +36 -0
- package/skills/ab/SKILL.md +1 -1
- package/skills/create-agent/SKILL.md +1 -1
- package/skills/debug/SKILL.md +1 -1
- package/skills/evals/SKILL.md +1 -1
- package/skills/framework-map/SKILL.md +1 -1
- package/skills/github/SKILL.md +1 -1
- package/skills/hillclimb/SKILL.md +1 -1
- package/skills/mcp-auth/SKILL.md +1 -1
- package/skills/otel/SKILL.md +1 -1
- package/skills/setup-slack/SKILL.md +1 -1
- package/src/bin/agent-serve.ts +7 -5
- package/src/channels/checks.ts +185 -0
- package/src/channels/deployments/deployments-channel.ts +109 -0
- package/src/channels/deployments/index.ts +26 -0
- package/src/channels/deployments/types.ts +80 -0
- package/src/channels/github/checks.ts +90 -115
- package/src/channels/origin/api.ts +318 -0
- package/src/channels/origin/binding.ts +96 -0
- package/src/channels/origin/checks.ts +250 -0
- package/src/channels/origin/index.ts +3 -0
- package/src/channels/origin/origin-channel.ts +18 -6
- package/src/channels/origin/types.ts +37 -0
- package/src/channels.ts +16 -0
- package/src/internal/cli-skills.ts +10 -2
- package/src/internal/cursor-deployment-relay.ts +274 -0
- package/src/internal/distribution.ts +1 -0
- package/src/internal/install-cursor-skills.ts +204 -49
- package/src/internal/sdk-runner.ts +6 -3
- package/src/internal/server.ts +149 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.DQad--_z.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CrWSA0Ko.js +0 -1
- package/dist/playground/assets/index-77uhu5Kr.css +0 -1
- package/dist/playground/assets/index-D4buox4m.js +0 -85
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull relay for `/v0/deployment-events`.
|
|
3
|
+
*
|
|
4
|
+
* A {@link RelayAdapter} over {@link CursorRelayCore} — the same engine the
|
|
5
|
+
* GitHub SCM and Slack relays use, so registration heartbeats, durable
|
|
6
|
+
* offsets, backoff, and bounded dispatch retry are inherited rather than
|
|
7
|
+
* reimplemented. Only the subscribe/poll paths, the registration body, and
|
|
8
|
+
* the per-event filter differ.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
DeploymentEvent,
|
|
13
|
+
DeploymentEventType,
|
|
14
|
+
DeploymentSubscription,
|
|
15
|
+
} from "../channels/deployments/types.js";
|
|
16
|
+
import { CursorRelayCore, type RelayAdapter } from "./cursor-relay-core.js";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_EVENTS: readonly DeploymentEventType[] = ["deployment.succeeded"];
|
|
19
|
+
|
|
20
|
+
/** One page entry: the event fields plus its stream offset, as the sibling
|
|
21
|
+
* providers spell it. */
|
|
22
|
+
export type DeploymentRelayEvent = DeploymentEvent & { offset: string };
|
|
23
|
+
|
|
24
|
+
export interface ResolvedSubscription {
|
|
25
|
+
repo: string;
|
|
26
|
+
environments: string[];
|
|
27
|
+
events: Set<DeploymentEventType>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SubscriptionStatus {
|
|
31
|
+
repo: string;
|
|
32
|
+
isSubscribed: boolean;
|
|
33
|
+
detail?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DeploymentEventRelayOptions {
|
|
37
|
+
baseUrl: string;
|
|
38
|
+
apiKey: string;
|
|
39
|
+
stateDir: string;
|
|
40
|
+
subscriptions: readonly ResolvedSubscription[];
|
|
41
|
+
dispatch: (event: DeploymentEvent) => Promise<void>;
|
|
42
|
+
logger: (line: string) => void;
|
|
43
|
+
fetchImpl?: typeof fetch;
|
|
44
|
+
pollWaitMs?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Normalize the channel's `repos` option into concrete subscriptions. */
|
|
48
|
+
export function resolveDeploymentSubscriptions(options: {
|
|
49
|
+
repos: readonly (string | DeploymentSubscription)[];
|
|
50
|
+
environments?: readonly string[];
|
|
51
|
+
events?: readonly DeploymentEventType[];
|
|
52
|
+
}): ResolvedSubscription[] {
|
|
53
|
+
const byRepo = new Map<string, ResolvedSubscription>();
|
|
54
|
+
for (const entry of options.repos) {
|
|
55
|
+
const subscription: DeploymentSubscription =
|
|
56
|
+
typeof entry === "string" ? { repo: entry } : entry;
|
|
57
|
+
// Case-folded only: `Deployment.repository` has no documented format, and
|
|
58
|
+
// the server keys subscriptions the same way.
|
|
59
|
+
const repo = subscription.repo.trim().toLowerCase();
|
|
60
|
+
if (repo === "") {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`deploymentsChannel: invalid repo "${subscription.repo}".`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const environments = [
|
|
66
|
+
...new Set(
|
|
67
|
+
(subscription.environments ?? options.environments ?? [])
|
|
68
|
+
.map((value) => value.trim())
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
),
|
|
71
|
+
];
|
|
72
|
+
const events = new Set(
|
|
73
|
+
subscription.events ?? options.events ?? DEFAULT_EVENTS
|
|
74
|
+
);
|
|
75
|
+
if (events.size === 0) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`deploymentsChannel: repo "${repo}" subscribes to no events.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
// Later entries for the same repo widen the earlier one rather than
|
|
81
|
+
// silently replacing it.
|
|
82
|
+
const existing = byRepo.get(repo);
|
|
83
|
+
if (existing === undefined) {
|
|
84
|
+
byRepo.set(repo, { repo, environments, events });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
for (const event of events) existing.events.add(event);
|
|
88
|
+
existing.environments =
|
|
89
|
+
existing.environments.length === 0 || environments.length === 0
|
|
90
|
+
? []
|
|
91
|
+
: [...new Set([...existing.environments, ...environments])];
|
|
92
|
+
}
|
|
93
|
+
// Empty is legal here and means "this channel wants nothing", which is how
|
|
94
|
+
// an env-configured channel stays inert without a repo list. The wire-level
|
|
95
|
+
// empty list means deregister, so the host simply never registers it.
|
|
96
|
+
return [...byRepo.values()];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether one confirmed subscription wants this event. */
|
|
100
|
+
export function deploymentEventMatches(
|
|
101
|
+
subscription: ResolvedSubscription,
|
|
102
|
+
envelope: DeploymentEvent
|
|
103
|
+
): boolean {
|
|
104
|
+
if (!subscription.events.has(envelope.eventType)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return (
|
|
108
|
+
subscription.environments.length === 0 ||
|
|
109
|
+
subscription.environments.includes(envelope.environment)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** One channel the host can deliver a deployment event to. */
|
|
114
|
+
export interface DeploymentDispatchTarget {
|
|
115
|
+
/** Display name for logs, e.g. `executor/deployments`. */
|
|
116
|
+
name: string;
|
|
117
|
+
subscriptions: readonly ResolvedSubscription[];
|
|
118
|
+
deliver: (event: DeploymentEvent) => Promise<unknown>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Deliver one event to every channel subscribed to it.
|
|
123
|
+
*
|
|
124
|
+
* Each channel is delivered independently: one channel's handler throwing
|
|
125
|
+
* must not skip the channels after it, because the relay core retries the
|
|
126
|
+
* whole dispatch and the channels that already ran would see the event twice.
|
|
127
|
+
* Throws only when every subscribed channel failed, so a retry cannot
|
|
128
|
+
* re-deliver to a channel that succeeded.
|
|
129
|
+
*/
|
|
130
|
+
export async function fanOutDeploymentEvent(input: {
|
|
131
|
+
targets: readonly DeploymentDispatchTarget[];
|
|
132
|
+
event: DeploymentEvent;
|
|
133
|
+
logger: (line: string) => void;
|
|
134
|
+
}): Promise<void> {
|
|
135
|
+
const { event, logger } = input;
|
|
136
|
+
const matched = input.targets.filter((target) =>
|
|
137
|
+
target.subscriptions.some(
|
|
138
|
+
(subscription) =>
|
|
139
|
+
subscription.repo === event.repo &&
|
|
140
|
+
deploymentEventMatches(subscription, event)
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
const failures: string[] = [];
|
|
144
|
+
for (const target of matched) {
|
|
145
|
+
try {
|
|
146
|
+
await target.deliver(event);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
failures.push(target.name);
|
|
149
|
+
logger(
|
|
150
|
+
`[agent-sdk] deployment-events: ${target.name} failed on ${event.eventId}: ${
|
|
151
|
+
error instanceof Error ? error.message : String(error)
|
|
152
|
+
}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
logger(
|
|
157
|
+
`[agent-sdk] deployment-events: ${event.eventType} ${event.repo}@${event.sha} (${event.environment}) → ${matched.length - failures.length}/${matched.length} channel(s)`
|
|
158
|
+
);
|
|
159
|
+
if (failures.length > 0 && failures.length === matched.length) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`deployment-events: every subscribed channel failed on ${event.eventId} (${failures.join(", ")})`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export class DeploymentEventRelay {
|
|
167
|
+
private readonly core: CursorRelayCore<DeploymentRelayEvent>;
|
|
168
|
+
private subscribed = new Map<string, ResolvedSubscription>();
|
|
169
|
+
private readonly loggedSkippedRepos = new Set<string>();
|
|
170
|
+
|
|
171
|
+
constructor(private readonly options: DeploymentEventRelayOptions) {
|
|
172
|
+
const adapter: RelayAdapter<DeploymentRelayEvent> = {
|
|
173
|
+
logPrefix: "deployment-events",
|
|
174
|
+
subscribePath: "/v0/deployment-events/subscriptions",
|
|
175
|
+
pollPath: "/v0/deployment-events",
|
|
176
|
+
buildRegistrationBody: (consumerId) => ({
|
|
177
|
+
consumerId,
|
|
178
|
+
subscriptions: options.subscriptions.map((subscription) => ({
|
|
179
|
+
repo: subscription.repo,
|
|
180
|
+
environments: subscription.environments,
|
|
181
|
+
events: [...subscription.events],
|
|
182
|
+
})),
|
|
183
|
+
}),
|
|
184
|
+
// Empty subscriptions drop this consumer's registration on clean stop
|
|
185
|
+
// instead of leaving it to lapse by TTL.
|
|
186
|
+
buildDeregistrationBody: (consumerId) => ({
|
|
187
|
+
consumerId,
|
|
188
|
+
subscriptions: [],
|
|
189
|
+
}),
|
|
190
|
+
applyRegistrationResponse: (json) => {
|
|
191
|
+
const result = json as { repos?: SubscriptionStatus[] };
|
|
192
|
+
this.subscribed = new Map();
|
|
193
|
+
this.loggedSkippedRepos.clear();
|
|
194
|
+
const lines: string[] = [];
|
|
195
|
+
const confirmed: string[] = [];
|
|
196
|
+
for (const status of result.repos ?? []) {
|
|
197
|
+
const wanted = options.subscriptions.find(
|
|
198
|
+
(subscription) => subscription.repo === status.repo
|
|
199
|
+
);
|
|
200
|
+
if (!status.isSubscribed || wanted === undefined) {
|
|
201
|
+
lines.push(
|
|
202
|
+
`not subscribed to ${status.repo}: ${status.detail ?? "unknown reason"}`
|
|
203
|
+
);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
this.subscribed.set(status.repo, wanted);
|
|
207
|
+
confirmed.push(status.repo);
|
|
208
|
+
}
|
|
209
|
+
if (confirmed.length > 0) {
|
|
210
|
+
const kinds = [
|
|
211
|
+
...new Set(options.subscriptions.flatMap((s) => [...s.events])),
|
|
212
|
+
];
|
|
213
|
+
lines.push(
|
|
214
|
+
`subscribed to ${confirmed.join(", ")} for events: ${kinds.join(", ")}`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return lines;
|
|
218
|
+
},
|
|
219
|
+
shouldDispatch: (event) => this.shouldDispatch(event),
|
|
220
|
+
dispatch: ({ offset: _offset, ...envelope }) =>
|
|
221
|
+
options.dispatch(envelope),
|
|
222
|
+
};
|
|
223
|
+
this.core = new CursorRelayCore<DeploymentRelayEvent>({
|
|
224
|
+
baseUrl: options.baseUrl,
|
|
225
|
+
apiKey: options.apiKey,
|
|
226
|
+
stateDir: options.stateDir,
|
|
227
|
+
adapter,
|
|
228
|
+
logger: options.logger,
|
|
229
|
+
...(options.fetchImpl === undefined
|
|
230
|
+
? {}
|
|
231
|
+
: { fetchImpl: options.fetchImpl }),
|
|
232
|
+
...(options.pollWaitMs === undefined
|
|
233
|
+
? {}
|
|
234
|
+
: { pollWaitMs: options.pollWaitMs }),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Whether an event from this principal's stream belongs to THIS relay.
|
|
240
|
+
* The stream is per-principal, and one API key can back several relays
|
|
241
|
+
* (other hosts, or an agent running its own alongside the host's), so each
|
|
242
|
+
* one re-checks the events against the subscriptions the server confirmed
|
|
243
|
+
* for it.
|
|
244
|
+
*/
|
|
245
|
+
private shouldDispatch(event: DeploymentRelayEvent): boolean {
|
|
246
|
+
const envelope = event;
|
|
247
|
+
if (
|
|
248
|
+
envelope === null ||
|
|
249
|
+
typeof envelope !== "object" ||
|
|
250
|
+
typeof envelope.repo !== "string"
|
|
251
|
+
) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
const subscription = this.subscribed.get(envelope.repo);
|
|
255
|
+
if (subscription === undefined) {
|
|
256
|
+
if (!this.loggedSkippedRepos.has(envelope.repo)) {
|
|
257
|
+
this.loggedSkippedRepos.add(envelope.repo);
|
|
258
|
+
this.options.logger(
|
|
259
|
+
`[agent-sdk] deployment-events: skipping events for ${envelope.repo}: not in this host's subscription (another consumer of this API key likely subscribed it)`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
return deploymentEventMatches(subscription, envelope);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
start(): void {
|
|
268
|
+
this.core.start();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async stop(): Promise<void> {
|
|
272
|
+
await this.core.stop();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -97,6 +97,7 @@ export const AUTHORING_ENTRY_FILES: ReadonlyArray<
|
|
|
97
97
|
["channels/slack", "channels/slack/index.js"],
|
|
98
98
|
["channels/github", "channels/github/index.js"],
|
|
99
99
|
["channels/origin", "channels/origin/index.js"],
|
|
100
|
+
["channels/deployments", "channels/deployments/index.js"],
|
|
100
101
|
["skills", "skills.js"],
|
|
101
102
|
["schedules", "schedules.js"],
|
|
102
103
|
["reminders", "reminders.js"],
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Offer to install the package's coding-agent skills into the user's
|
|
3
|
-
* personal Cursor skills dir under `~/.cursor/skills/
|
|
3
|
+
* personal Cursor skills dir under `~/.cursor/skills/agentsdk/<skill>/`.
|
|
4
4
|
*
|
|
5
|
-
* Cursor walks `~/.cursor/skills` recursively, so the `
|
|
5
|
+
* Cursor walks `~/.cursor/skills` recursively, so the `agentsdk/` folder is
|
|
6
6
|
* only organizational — identity comes from the leaf folder that holds
|
|
7
|
-
* SKILL.md.
|
|
7
|
+
* SKILL.md. Installed copies get `alwaysApply: true` so Cursor injects the
|
|
8
|
+
* skill body as a global rule instead of leaving it as an on-demand catalog
|
|
9
|
+
* entry. Skipped for `--json` / non-TTY hosts.
|
|
10
|
+
*
|
|
11
|
+
* Consumer installs (`npm install` / `npx` of `@cursor/july`) also copy
|
|
12
|
+
* these skills from a postinstall hook. That path never prompts and never
|
|
13
|
+
* fails the package install. The monorepo source checkout is not a consumer
|
|
14
|
+
* install, so `pnpm install` in everysphere does not overwrite `~/.cursor`.
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
17
|
import {
|
|
@@ -21,51 +28,86 @@ import {
|
|
|
21
28
|
import { homedir } from "node:os";
|
|
22
29
|
import { join } from "node:path";
|
|
23
30
|
import { CLI_COMMAND_NAME, packageRootDir } from "./distribution.js";
|
|
31
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
24
32
|
import { stdoutPalette } from "./terminal-style.js";
|
|
25
33
|
|
|
26
34
|
/** Monorepo-only skill; excluded from the published package. */
|
|
27
35
|
const SKIP_SKILL_DIRS = new Set(["operate"]);
|
|
28
36
|
|
|
29
37
|
/** Category folder under ~/.cursor/skills (Cursor discovers nested skills). */
|
|
30
|
-
export const CURSOR_SKILLS_NAMESPACE = "
|
|
38
|
+
export const CURSOR_SKILLS_NAMESPACE = "agentsdk";
|
|
39
|
+
|
|
40
|
+
/** Pre-rename install folder. Install deletes this so `/agentkit-*` rows disappear. */
|
|
41
|
+
export const LEGACY_CURSOR_SKILLS_NAMESPACE = "agentkit";
|
|
31
42
|
|
|
32
43
|
/** How long the `init` offer waits for an answer before skipping the install. */
|
|
33
44
|
export const SKILL_PROMPT_TIMEOUT_MS = 45_000;
|
|
34
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Override the resolved `~/.cursor/skills` directory (not the user home).
|
|
48
|
+
* Used by the postinstall hook in tests and publish smoke.
|
|
49
|
+
*/
|
|
50
|
+
export const CURSOR_JULY_SKILLS_HOME_ENV = "CURSOR_JULY_SKILLS_HOME";
|
|
51
|
+
|
|
52
|
+
/** Set to `1` to skip the postinstall skill copy. */
|
|
53
|
+
export const CURSOR_JULY_SKIP_SKILL_INSTALL_ENV =
|
|
54
|
+
"CURSOR_JULY_SKIP_SKILL_INSTALL";
|
|
55
|
+
|
|
35
56
|
export interface PackageSkill {
|
|
36
|
-
/** Frontmatter `name` (e.g.
|
|
57
|
+
/** Frontmatter `name` (e.g. agentsdk-create-agent). */
|
|
37
58
|
name: string;
|
|
38
|
-
/** Leaf install directory under
|
|
59
|
+
/** Leaf install directory under agentsdk/ (e.g. create-agent). */
|
|
39
60
|
leafDir: string;
|
|
40
61
|
/** Absolute path to the source skill directory in the package. */
|
|
41
62
|
sourceDir: string;
|
|
42
63
|
}
|
|
43
64
|
|
|
44
|
-
export function cursorSkillsHome(
|
|
45
|
-
home
|
|
46
|
-
):
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
export function cursorSkillsHome(home?: string): string {
|
|
66
|
+
if (home !== undefined) {
|
|
67
|
+
return join(home.trim() === "" ? homedir() : home, ".cursor", "skills");
|
|
68
|
+
}
|
|
69
|
+
const override = process.env[CURSOR_JULY_SKILLS_HOME_ENV]?.trim();
|
|
70
|
+
if (override !== undefined && override !== "") {
|
|
71
|
+
return override;
|
|
72
|
+
}
|
|
73
|
+
return join(homedir(), ".cursor", "skills");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* True when this package root is an installed copy under `node_modules`
|
|
78
|
+
* (npm, pnpm, yarn, npx, global), not the everysphere source checkout.
|
|
79
|
+
*/
|
|
80
|
+
export function isConsumerPackageInstall(packageRoot: string): boolean {
|
|
81
|
+
// Segment check only — split on both separators so a Windows path still
|
|
82
|
+
// matches when this runs (or is tested) on POSIX. Do not rewrite the path.
|
|
83
|
+
return packageRoot.split(/[\\/]/).includes("node_modules");
|
|
49
84
|
}
|
|
50
85
|
|
|
51
|
-
/** `~/.cursor/skills/
|
|
52
|
-
export function
|
|
86
|
+
/** `~/.cursor/skills/agentsdk` — where package skills are installed. */
|
|
87
|
+
export function agentsdkCursorSkillsDir(
|
|
53
88
|
skillsHome: string = cursorSkillsHome()
|
|
54
89
|
): string {
|
|
55
90
|
return join(skillsHome, CURSOR_SKILLS_NAMESPACE);
|
|
56
91
|
}
|
|
57
92
|
|
|
58
|
-
/**
|
|
93
|
+
/** Pre-rename `~/.cursor/skills/agentkit`. */
|
|
94
|
+
export function legacyAgentkitCursorSkillsDir(
|
|
95
|
+
skillsHome: string = cursorSkillsHome()
|
|
96
|
+
): string {
|
|
97
|
+
return join(skillsHome, LEGACY_CURSOR_SKILLS_NAMESPACE);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Absolute install path for one skill: `…/agentsdk/<leafDir>`. */
|
|
59
101
|
export function skillInstallPath(
|
|
60
102
|
skill: Pick<PackageSkill, "leafDir">,
|
|
61
103
|
skillsHome: string = cursorSkillsHome()
|
|
62
104
|
): string {
|
|
63
|
-
return join(
|
|
105
|
+
return join(agentsdkCursorSkillsDir(skillsHome), skill.leafDir);
|
|
64
106
|
}
|
|
65
107
|
|
|
66
108
|
/**
|
|
67
109
|
* Skills shipped next to the running package. Uses the frontmatter `name`
|
|
68
|
-
* when present, otherwise `
|
|
110
|
+
* when present, otherwise `agentsdk-<dir>`.
|
|
69
111
|
*/
|
|
70
112
|
export async function listShippedPackageSkills(
|
|
71
113
|
packageRoot: string = packageRootDir()
|
|
@@ -91,7 +133,9 @@ export async function listShippedPackageSkills(
|
|
|
91
133
|
continue;
|
|
92
134
|
}
|
|
93
135
|
const raw = await readFile(skillMd, "utf8");
|
|
94
|
-
const name =
|
|
136
|
+
const name =
|
|
137
|
+
parseFrontmatter(raw).data.name ||
|
|
138
|
+
`${CURSOR_SKILLS_NAMESPACE}-${entry.name}`;
|
|
95
139
|
skills.push({ name, leafDir: entry.name, sourceDir });
|
|
96
140
|
}
|
|
97
141
|
skills.sort((a, b) => a.leafDir.localeCompare(b.leafDir));
|
|
@@ -99,7 +143,7 @@ export async function listShippedPackageSkills(
|
|
|
99
143
|
}
|
|
100
144
|
|
|
101
145
|
/**
|
|
102
|
-
* Skills that do not yet have a SKILL.md under ~/.cursor/skills/
|
|
146
|
+
* Skills that do not yet have a SKILL.md under ~/.cursor/skills/agentsdk/.
|
|
103
147
|
* Install still overwrites every shipped skill when the user confirms — this
|
|
104
148
|
* helper is for messaging (fresh install vs update).
|
|
105
149
|
*/
|
|
@@ -125,18 +169,22 @@ export interface SkillInstallResult {
|
|
|
125
169
|
installed: string[];
|
|
126
170
|
/** Leaf dirs deleted because the package no longer ships them. */
|
|
127
171
|
removed: string[];
|
|
172
|
+
/** True when leftover `~/.cursor/skills/agentkit` was deleted. */
|
|
173
|
+
removedLegacyNamespace: boolean;
|
|
128
174
|
}
|
|
129
175
|
|
|
130
176
|
/**
|
|
131
|
-
* Copy each skill directory into `skillsHome/
|
|
177
|
+
* Copy each skill directory into `skillsHome/agentsdk/<leafDir>/`.
|
|
132
178
|
* Always overwrites so a package upgrade refreshes local Cursor skills.
|
|
133
|
-
* Strips monorepo-only frontmatter
|
|
179
|
+
* Strips monorepo-only frontmatter, sets `alwaysApply: true` so Cursor
|
|
180
|
+
* loads the body into context, and removes leaf dirs no longer shipped.
|
|
181
|
+
* Also deletes leftover `skillsHome/agentkit/` from the previous namespace.
|
|
134
182
|
*/
|
|
135
183
|
export async function installCursorSkills(
|
|
136
184
|
skills: readonly PackageSkill[],
|
|
137
185
|
skillsHome: string = cursorSkillsHome()
|
|
138
186
|
): Promise<SkillInstallResult> {
|
|
139
|
-
const namespaceDir =
|
|
187
|
+
const namespaceDir = agentsdkCursorSkillsDir(skillsHome);
|
|
140
188
|
await mkdir(namespaceDir, { recursive: true });
|
|
141
189
|
|
|
142
190
|
const removed = await orphanLeaves(namespaceDir, skills);
|
|
@@ -161,7 +209,9 @@ export async function installCursorSkills(
|
|
|
161
209
|
}
|
|
162
210
|
installed.push(skill.leafDir);
|
|
163
211
|
}
|
|
164
|
-
|
|
212
|
+
const removedLegacyNamespace =
|
|
213
|
+
await removeLegacyCursorSkillsNamespace(skillsHome);
|
|
214
|
+
return { installed, removed, removedLegacyNamespace };
|
|
165
215
|
}
|
|
166
216
|
|
|
167
217
|
async function makeTreeWritable(root: string): Promise<void> {
|
|
@@ -202,13 +252,43 @@ async function orphanLeaves(
|
|
|
202
252
|
.map((entry) => entry.name);
|
|
203
253
|
}
|
|
204
254
|
|
|
205
|
-
/** Drop
|
|
255
|
+
/** Drop leftover `skillsHome/agentkit` from the previous install namespace. */
|
|
256
|
+
async function removeLegacyCursorSkillsNamespace(
|
|
257
|
+
skillsHome: string
|
|
258
|
+
): Promise<boolean> {
|
|
259
|
+
const legacyDir = legacyAgentkitCursorSkillsDir(skillsHome);
|
|
260
|
+
try {
|
|
261
|
+
await stat(legacyDir);
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
await rm(legacyDir, { recursive: true, force: true });
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function hasLegacyCursorSkillsNamespace(
|
|
270
|
+
skillsHome: string
|
|
271
|
+
): Promise<boolean> {
|
|
272
|
+
try {
|
|
273
|
+
await stat(legacyAgentkitCursorSkillsDir(skillsHome));
|
|
274
|
+
return true;
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Drop monorepo `paths` globs, set `alwaysApply: true` so Cursor injects the
|
|
282
|
+
* body as a global rule, and fix package doc links for ~/.cursor installs.
|
|
283
|
+
*/
|
|
206
284
|
export function prepareSkillMarkdownForCursorInstall(content: string): string {
|
|
207
285
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
208
286
|
if (match === null) {
|
|
209
287
|
return content;
|
|
210
288
|
}
|
|
211
|
-
const frontmatter =
|
|
289
|
+
const frontmatter = withAlwaysApply(
|
|
290
|
+
stripPathsFromFrontmatter(match[1] ?? "")
|
|
291
|
+
);
|
|
212
292
|
const body = content.slice(match[0].length);
|
|
213
293
|
const adjustedBody = body.replace(
|
|
214
294
|
/\[`docs\/([^`]+)`\]\(\.\.\/\.\.\/docs\/[^)]+\)/g,
|
|
@@ -235,11 +315,24 @@ function stripPathsFromFrontmatter(frontmatter: string): string {
|
|
|
235
315
|
return out.join("\n");
|
|
236
316
|
}
|
|
237
317
|
|
|
318
|
+
/** Cursor loads `alwaysApply: true` skills as global rules, not catalog rows. */
|
|
319
|
+
function withAlwaysApply(frontmatter: string): string {
|
|
320
|
+
const without = frontmatter
|
|
321
|
+
.split(/\r?\n/)
|
|
322
|
+
.filter((line) => !/^\s*alwaysApply\s*:/.test(line))
|
|
323
|
+
.join("\n")
|
|
324
|
+
.replace(/\n+$/, "");
|
|
325
|
+
if (without === "") {
|
|
326
|
+
return "alwaysApply: true";
|
|
327
|
+
}
|
|
328
|
+
return `${without}\nalwaysApply: true`;
|
|
329
|
+
}
|
|
330
|
+
|
|
238
331
|
export interface SkillInstallPlan {
|
|
239
332
|
shipped: PackageSkill[];
|
|
240
333
|
/** Resolved `~/.cursor/skills`, to install into. */
|
|
241
334
|
skillsHome: string;
|
|
242
|
-
/** Install target: `<skillsHome>/
|
|
335
|
+
/** Install target: `<skillsHome>/agentsdk`. */
|
|
243
336
|
directory: string;
|
|
244
337
|
/** Leaf dirs the package ships. */
|
|
245
338
|
leaves: string[];
|
|
@@ -247,6 +340,8 @@ export interface SkillInstallPlan {
|
|
|
247
340
|
firstInstall: boolean;
|
|
248
341
|
/** Installed leaf dirs the package dropped; installing deletes them. */
|
|
249
342
|
orphans: string[];
|
|
343
|
+
/** True when leftover `skillsHome/agentkit` still exists. */
|
|
344
|
+
hasLegacyNamespace: boolean;
|
|
250
345
|
}
|
|
251
346
|
|
|
252
347
|
/** What an install would do, for the offer and for `install-skills`. */
|
|
@@ -255,19 +350,26 @@ export async function planSkillInstall(options?: {
|
|
|
255
350
|
skillsHome?: string;
|
|
256
351
|
}): Promise<SkillInstallPlan> {
|
|
257
352
|
const skillsHome = options?.skillsHome ?? cursorSkillsHome();
|
|
258
|
-
const directory =
|
|
353
|
+
const directory = agentsdkCursorSkillsDir(skillsHome);
|
|
259
354
|
const shipped = await listShippedPackageSkills(options?.packageRoot);
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
355
|
+
const present = await Promise.all(
|
|
356
|
+
shipped.map(async (skill) => {
|
|
357
|
+
try {
|
|
358
|
+
await access(join(skillInstallPath(skill, skillsHome), "SKILL.md"));
|
|
359
|
+
return true;
|
|
360
|
+
} catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
);
|
|
264
365
|
return {
|
|
265
366
|
shipped,
|
|
266
367
|
skillsHome,
|
|
267
368
|
directory,
|
|
268
369
|
leaves: shipped.map((skill) => skill.leafDir),
|
|
269
|
-
firstInstall:
|
|
370
|
+
firstInstall: present.every((isPresent) => !isPresent),
|
|
270
371
|
orphans: await orphanLeaves(directory, shipped),
|
|
372
|
+
hasLegacyNamespace: await hasLegacyCursorSkillsNamespace(skillsHome),
|
|
271
373
|
};
|
|
272
374
|
}
|
|
273
375
|
|
|
@@ -282,10 +384,73 @@ export function formatSkillInstall(
|
|
|
282
384
|
if (result.removed.length > 0) {
|
|
283
385
|
lines.push(` removed: ${result.removed.join(", ")}`);
|
|
284
386
|
}
|
|
387
|
+
if (result.removedLegacyNamespace) {
|
|
388
|
+
lines.push(
|
|
389
|
+
` removed: ${legacyAgentkitCursorSkillsDir(plan.skillsHome)} (legacy namespace)`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
285
392
|
lines.push(` location: ${plan.directory}`);
|
|
286
393
|
return `${lines.join("\n")}\n`;
|
|
287
394
|
}
|
|
288
395
|
|
|
396
|
+
export type PackageInstallSkillSyncResult =
|
|
397
|
+
| { status: "installed"; installed: string[]; removed: string[] }
|
|
398
|
+
| {
|
|
399
|
+
status: "skipped";
|
|
400
|
+
reason: "opt-out" | "not-consumer" | "no-skills" | "error";
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
export interface SyncCursorSkillsOnPackageInstallOptions {
|
|
404
|
+
packageRoot?: string;
|
|
405
|
+
skillsHome?: string;
|
|
406
|
+
env?: NodeJS.ProcessEnv;
|
|
407
|
+
/** Override the node_modules consumer check. */
|
|
408
|
+
consumer?: boolean;
|
|
409
|
+
out?: (text: string) => void;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Copy shipped coding-agent skills into ~/.cursor/skills/agentsdk during a
|
|
414
|
+
* consumer `npm install` / `npx`. Never throws: a failed copy must not fail
|
|
415
|
+
* the package install. No-op in the monorepo source tree and when
|
|
416
|
+
* `CURSOR_JULY_SKIP_SKILL_INSTALL=1`.
|
|
417
|
+
*/
|
|
418
|
+
export async function syncCursorSkillsOnPackageInstall(
|
|
419
|
+
options: SyncCursorSkillsOnPackageInstallOptions = {}
|
|
420
|
+
): Promise<PackageInstallSkillSyncResult> {
|
|
421
|
+
try {
|
|
422
|
+
const env = options.env ?? process.env;
|
|
423
|
+
if (env[CURSOR_JULY_SKIP_SKILL_INSTALL_ENV] === "1") {
|
|
424
|
+
return { status: "skipped", reason: "opt-out" };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const packageRoot = options.packageRoot ?? packageRootDir();
|
|
428
|
+
const consumer = options.consumer ?? isConsumerPackageInstall(packageRoot);
|
|
429
|
+
if (!consumer) {
|
|
430
|
+
return { status: "skipped", reason: "not-consumer" };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const plan = await planSkillInstall({
|
|
434
|
+
packageRoot,
|
|
435
|
+
skillsHome: options.skillsHome,
|
|
436
|
+
});
|
|
437
|
+
if (plan.shipped.length === 0) {
|
|
438
|
+
return { status: "skipped", reason: "no-skills" };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const result = await installCursorSkills(plan.shipped, plan.skillsHome);
|
|
442
|
+
const out = options.out ?? ((text) => process.stderr.write(text));
|
|
443
|
+
out(formatSkillInstall(plan, result));
|
|
444
|
+
return {
|
|
445
|
+
status: "installed",
|
|
446
|
+
installed: result.installed,
|
|
447
|
+
removed: result.removed,
|
|
448
|
+
};
|
|
449
|
+
} catch {
|
|
450
|
+
return { status: "skipped", reason: "error" };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
289
454
|
export interface OfferInstallCursorSkillsOptions {
|
|
290
455
|
/** Skip when false (non-TTY / `--json`). */
|
|
291
456
|
interactive?: boolean;
|
|
@@ -298,7 +463,7 @@ export interface OfferInstallCursorSkillsOptions {
|
|
|
298
463
|
|
|
299
464
|
/**
|
|
300
465
|
* On a TTY, offer to install/update every shipped package skill into
|
|
301
|
-
* ~/.cursor/skills/
|
|
466
|
+
* ~/.cursor/skills/agentsdk (always overwriting). No-op when non-interactive
|
|
302
467
|
* or the package ships no skills. An unanswered prompt skips the install
|
|
303
468
|
* rather than blocking the caller — see `SKILL_PROMPT_TIMEOUT_MS`.
|
|
304
469
|
*/
|
|
@@ -334,6 +499,13 @@ export async function offerInstallCursorSkills(
|
|
|
334
499
|
if (plan.orphans.length > 0) {
|
|
335
500
|
out(c.dim(` (deletes ${plan.orphans.join(", ")})\n`));
|
|
336
501
|
}
|
|
502
|
+
if (plan.hasLegacyNamespace) {
|
|
503
|
+
out(
|
|
504
|
+
c.dim(
|
|
505
|
+
` (deletes leftover ${legacyAgentkitCursorSkillsDir(plan.skillsHome)})\n`
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
}
|
|
337
509
|
|
|
338
510
|
const confirm =
|
|
339
511
|
options.confirm ??
|
|
@@ -365,23 +537,6 @@ export async function offerInstallCursorSkills(
|
|
|
365
537
|
return { installed: result.installed, skipped: false };
|
|
366
538
|
}
|
|
367
539
|
|
|
368
|
-
function parseSkillName(skillMd: string): string | undefined {
|
|
369
|
-
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
|
|
370
|
-
if (match === null) {
|
|
371
|
-
return undefined;
|
|
372
|
-
}
|
|
373
|
-
const nameLine = /^name:\s*(.+)\s*$/m.exec(match[1] ?? "");
|
|
374
|
-
if (nameLine === null) {
|
|
375
|
-
return undefined;
|
|
376
|
-
}
|
|
377
|
-
const raw = nameLine[1]?.trim();
|
|
378
|
-
if (raw === undefined || raw === "") {
|
|
379
|
-
return undefined;
|
|
380
|
-
}
|
|
381
|
-
// Strip optional quotes around the YAML scalar.
|
|
382
|
-
return raw.replace(/^["']|["']$/g, "");
|
|
383
|
-
}
|
|
384
|
-
|
|
385
540
|
export interface PromptYesNoOptions {
|
|
386
541
|
defaultYes: boolean;
|
|
387
542
|
out: (text: string) => void;
|