@cursor/july 0.1.23 → 0.1.25

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.
@@ -491,6 +491,16 @@ interface ConnectWaitTarget {
491
491
  notConnectedAt?: string;
492
492
  }
493
493
 
494
+ /**
495
+ * Slack's team-scoped admin page listing pending app-install requests.
496
+ * Provisioned apps have no OAuth flow (no redirect URLs, install-from-Slack
497
+ * disabled), so the oauth_authorize_url Slack returns from
498
+ * apps.manifest.create dead-ends; approval happens here instead.
499
+ */
500
+ function slackAppRequestsUrl(slackTeamId: string): string {
501
+ return `https://app.slack.com/apps-manage/${encodeURIComponent(slackTeamId)}/integrations/requests`;
502
+ }
503
+
494
504
  /** "Name (T0123ABCD)", or the bare id when the backend has no name. */
495
505
  function formatWorkspace(workspace: SlackConnectedWorkspace): string {
496
506
  const name = workspace.slackTeamName ?? "";
@@ -719,6 +729,7 @@ export async function cmdSlackCreate(
719
729
  const iconStatus = await applySlackCreateIcon(ctx, iconSource, result);
720
730
 
721
731
  if (result.status === "pending_admin_approval") {
732
+ const approveUrl = slackAppRequestsUrl(workspace.slackTeamId);
722
733
  if (ctx.json) {
723
734
  ctx.out(
724
735
  `${JSON.stringify(
@@ -729,7 +740,7 @@ export async function cmdSlackCreate(
729
740
  slackTeamId: workspace.slackTeamId,
730
741
  envKeys: target.envKeys,
731
742
  wrote,
732
- oauthAuthorizeUrl: result.oauthAuthorizeUrl ?? null,
743
+ approveUrl,
733
744
  iconStatus,
734
745
  },
735
746
  null,
@@ -739,9 +750,7 @@ export async function cmdSlackCreate(
739
750
  } else {
740
751
  ctx.err(
741
752
  `The Slack app was created (app ${result.appId}), but installing it needs a Slack workspace-admin approval.\n` +
742
- (result.oauthAuthorizeUrl === undefined
743
- ? ""
744
- : `\n Approve it (or send this to a workspace admin):\n\n ${result.oauthAuthorizeUrl}\n\n`) +
753
+ `\n A workspace admin can approve it here:\n\n ${approveUrl}\n\n` +
745
754
  `Once an admin approves the app, re-run \`${CLI} slack create\` to finish the install and mint the tokens.\n`
746
755
  );
747
756
  }
@@ -0,0 +1,136 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { defineArtifacts } from "../artifacts.js";
6
+ import { defineAgent } from "../index.js";
7
+ import { defineTool } from "../tools.js";
8
+ import type { AgentConfig, ArtifactsConfig } from "../types.js";
9
+ import { loadAgentProject, projectInfo } from "./discovery.js";
10
+
11
+ const cleanups: Array<() => Promise<void>> = [];
12
+
13
+ afterEach(async () => {
14
+ while (cleanups.length > 0) {
15
+ await cleanups.pop()?.();
16
+ }
17
+ });
18
+
19
+ const TAGGABLE: ArtifactsConfig = {
20
+ kinds: {
21
+ "reviewed-pr": { description: "A reviewed PR." },
22
+ report: { description: "A report." },
23
+ },
24
+ agentTool: true,
25
+ };
26
+
27
+ async function scaffold(options: {
28
+ agent?: AgentConfig;
29
+ artifacts?: ArtifactsConfig;
30
+ authoredTools?: string[];
31
+ }) {
32
+ const rootDir = await mkdtemp(join(tmpdir(), "agent-serve-artifact-tool-"));
33
+ cleanups.push(() => rm(rootDir, { recursive: true, force: true }));
34
+ const agentDir = join(rootDir, "agent");
35
+ await mkdir(agentDir, { recursive: true });
36
+ await writeFile(join(agentDir, "agent.ts"), "export default {};\n");
37
+ await writeFile(join(agentDir, "instructions.md"), "Be brief.\n");
38
+ if (options.artifacts !== undefined) {
39
+ await writeFile(join(agentDir, "artifacts.ts"), "export default {};\n");
40
+ }
41
+ for (const name of options.authoredTools ?? []) {
42
+ await mkdir(join(agentDir, "tools"), { recursive: true });
43
+ await writeFile(
44
+ join(agentDir, "tools", `${name}.ts`),
45
+ "export default {};\n"
46
+ );
47
+ }
48
+ return loadAgentProject(rootDir, {
49
+ importModule: async (path) => {
50
+ if (path.endsWith("artifacts.ts")) {
51
+ return { default: defineArtifacts(options.artifacts ?? {}) };
52
+ }
53
+ if (path.includes(join("agent", "tools"))) {
54
+ return {
55
+ default: defineTool({
56
+ description: "Authored tool.",
57
+ execute: () => "authored",
58
+ }),
59
+ };
60
+ }
61
+ return { default: defineAgent(options.agent ?? {}) };
62
+ },
63
+ });
64
+ }
65
+
66
+ describe("loadAgentProject tag_artifact built-in", () => {
67
+ it("materializes tag_artifact when agentTool is true", async () => {
68
+ const project = await scaffold({ artifacts: TAGGABLE });
69
+ const tool = project.agent.tools.find((t) => t.name === "tag_artifact");
70
+ expect(tool).toMatchObject({
71
+ name: "tag_artifact",
72
+ execution: "server",
73
+ needsApproval: false,
74
+ });
75
+ expect(tool?.description).toContain("- reviewed-pr: A reviewed PR.");
76
+ expect(project.diagnostics).toEqual([]);
77
+
78
+ // Info/manifest surface it exactly like authored tools.
79
+ const info = projectInfo(project);
80
+ const infoTool = info.tools.find((t) => t.name === "tag_artifact");
81
+ expect(infoTool?.execution).toBe("server");
82
+ expect(infoTool?.inputSchema).toBeDefined();
83
+ expect(info.artifacts?.agentTool).toBe(true);
84
+ });
85
+
86
+ it("stays absent without agentTool", async () => {
87
+ const withoutFlag = await scaffold({
88
+ artifacts: { kinds: { report: { description: "A report." } } },
89
+ });
90
+ expect(withoutFlag.agent.tools.some((t) => t.name === "tag_artifact")).toBe(
91
+ false
92
+ );
93
+
94
+ const unauthored = await scaffold({});
95
+ expect(unauthored.agent.tools.some((t) => t.name === "tag_artifact")).toBe(
96
+ false
97
+ );
98
+ });
99
+
100
+ it("skips the tool on cloud runtime, with a warning", async () => {
101
+ const project = await scaffold({
102
+ agent: {
103
+ runtime: "cloud",
104
+ cloud: { repos: [{ url: "https://github.com/org/repo" }] },
105
+ },
106
+ artifacts: TAGGABLE,
107
+ });
108
+ expect(project.agent.tools.some((t) => t.name === "tag_artifact")).toBe(
109
+ false
110
+ );
111
+ expect(
112
+ project.diagnostics.some(
113
+ (d) =>
114
+ d.severity === "warning" &&
115
+ d.message.includes("tag_artifact") &&
116
+ d.message.includes("not available on cloud agents")
117
+ )
118
+ ).toBe(true);
119
+ });
120
+
121
+ it("lets an authored tag_artifact shadow the built-in, with a warning", async () => {
122
+ const project = await scaffold({
123
+ artifacts: TAGGABLE,
124
+ authoredTools: ["tag_artifact"],
125
+ });
126
+ const tools = project.agent.tools.filter((t) => t.name === "tag_artifact");
127
+ expect(tools).toHaveLength(1);
128
+ expect(tools[0]?.description).toBe("Authored tool.");
129
+ expect(project.diagnostics).toEqual([
130
+ expect.objectContaining({
131
+ severity: "warning",
132
+ message: expect.stringContaining("shadows the built-in tool"),
133
+ }),
134
+ ]);
135
+ });
136
+ });
@@ -0,0 +1,119 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { defineArtifacts } from "../artifacts.js";
6
+ import { defineAgent } from "../index.js";
7
+ import type { ArtifactsConfig } from "../types.js";
8
+ import { loadAgentProject, projectInfo } from "./discovery.js";
9
+
10
+ const cleanups: Array<() => Promise<void>> = [];
11
+
12
+ afterEach(async () => {
13
+ while (cleanups.length > 0) {
14
+ await cleanups.pop()?.();
15
+ }
16
+ });
17
+
18
+ async function scaffold(options: {
19
+ artifacts?: ArtifactsConfig;
20
+ subagentArtifacts?: boolean;
21
+ }) {
22
+ const rootDir = await mkdtemp(join(tmpdir(), "agent-serve-artifacts-disc-"));
23
+ cleanups.push(() => rm(rootDir, { recursive: true, force: true }));
24
+ const agentDir = join(rootDir, "agent");
25
+ await mkdir(agentDir, { recursive: true });
26
+ await writeFile(join(agentDir, "agent.ts"), "export default {};\n");
27
+ await writeFile(join(agentDir, "instructions.md"), "Be brief.\n");
28
+ if (options.artifacts !== undefined) {
29
+ await writeFile(join(agentDir, "artifacts.ts"), "export default {};\n");
30
+ }
31
+ if (options.subagentArtifacts === true) {
32
+ const subagentDir = join(agentDir, "subagents", "helper");
33
+ await mkdir(subagentDir, { recursive: true });
34
+ await writeFile(join(subagentDir, "agent.ts"), "export default {};\n");
35
+ await writeFile(join(subagentDir, "instructions.md"), "Help.\n");
36
+ await writeFile(join(subagentDir, "artifacts.ts"), "export default {};\n");
37
+ }
38
+ return loadAgentProject(rootDir, {
39
+ importModule: async (path) => {
40
+ if (path.endsWith("artifacts.ts")) {
41
+ return { default: defineArtifacts(options.artifacts ?? {}) };
42
+ }
43
+ if (path.includes("subagents")) {
44
+ return { default: defineAgent({ description: "Helper." }) };
45
+ }
46
+ return { default: defineAgent({}) };
47
+ },
48
+ });
49
+ }
50
+
51
+ describe("loadAgentProject artifacts", () => {
52
+ it("discovers agent/artifacts.ts and surfaces it on the manifest and info", async () => {
53
+ const project = await scaffold({
54
+ artifacts: {
55
+ kinds: {
56
+ "reviewed-pr": { description: "A reviewed PR." },
57
+ report: { description: "A report." },
58
+ },
59
+ max: 50,
60
+ },
61
+ });
62
+ expect(project.artifacts).toBeDefined();
63
+ expect(project.diagnostics).toEqual([]);
64
+ expect(projectInfo(project).artifacts).toEqual({
65
+ kinds: ["reviewed-pr", "report"],
66
+ agentTool: false,
67
+ max: 50,
68
+ });
69
+ });
70
+
71
+ it("defaults max to 1000 on info and stays absent when unauthored", async () => {
72
+ const withDefaults = await scaffold({ artifacts: { kinds: {} } });
73
+ expect(projectInfo(withDefaults).artifacts).toEqual({
74
+ kinds: [],
75
+ agentTool: false,
76
+ max: 1000,
77
+ });
78
+
79
+ const without = await scaffold({});
80
+ expect(without.artifacts).toBeUndefined();
81
+ expect(projectInfo(without).artifacts).toBeUndefined();
82
+ });
83
+
84
+ it("errors when agentTool is set without declared kinds", async () => {
85
+ const project = await scaffold({ artifacts: { agentTool: true } });
86
+ expect(project.diagnostics).toEqual([
87
+ expect.objectContaining({
88
+ severity: "error",
89
+ message: expect.stringContaining(
90
+ "the tag_artifact tool needs at least one declared kind"
91
+ ),
92
+ }),
93
+ ]);
94
+ });
95
+
96
+ it("accepts agentTool with at least one kind (schema optional)", async () => {
97
+ const project = await scaffold({
98
+ artifacts: {
99
+ kinds: { note: { description: "A note." } },
100
+ agentTool: true,
101
+ },
102
+ });
103
+ expect(project.diagnostics).toEqual([]);
104
+ expect(projectInfo(project).artifacts?.agentTool).toBe(true);
105
+ });
106
+
107
+ it("warns and ignores artifacts on subagents", async () => {
108
+ const project = await scaffold({ subagentArtifacts: true });
109
+ expect(project.diagnostics).toEqual([
110
+ expect.objectContaining({
111
+ severity: "warning",
112
+ message: expect.stringContaining(
113
+ "artifacts are not supported on subagents"
114
+ ),
115
+ }),
116
+ ]);
117
+ expect(project.artifacts).toBeUndefined();
118
+ });
119
+ });
@@ -18,6 +18,7 @@ import {
18
18
  } from "node:path";
19
19
  import { pathToFileURL } from "node:url";
20
20
  import { type ABConfigFile, resolveABMaxPlaygroundSessions } from "../ab.js";
21
+ import { resolveArtifactsMax } from "../artifacts.js";
21
22
  import { httpChannel } from "../channels.js";
22
23
  import type { StorageDefinition } from "../storage.js";
23
24
  import {
@@ -27,6 +28,7 @@ import {
27
28
  type AgentProject,
28
29
  type AgentProjectInfo,
29
30
  type AgentRuntime,
31
+ type ArtifactsDefinition,
30
32
  type BuiltinToolsConfig,
31
33
  type ChannelDefinition,
32
34
  type ConnectionDefinition,
@@ -58,7 +60,11 @@ import {
58
60
  import { registerAuthoredModuleLoaders } from "./authored-loaders.js";
59
61
  import { normalizePositiveInt } from "./bounded-int.js";
60
62
  import { getDefinitionKind } from "./brand.js";
61
- import { builtinTools } from "./builtin-tools/index.js";
63
+ import {
64
+ artifactBuiltinTool,
65
+ builtinTools,
66
+ TAG_ARTIFACT_TOOL_NAME,
67
+ } from "./builtin-tools/index.js";
62
68
  import { isValidCron } from "./cron.js";
63
69
  import { parseFrontmatter } from "./frontmatter.js";
64
70
  import {
@@ -280,6 +286,8 @@ export async function loadAgentProject(
280
286
  const abs = await loadABs(ctx, agentDir);
281
287
  const abConfig = await loadABConfig(ctx, agentDir);
282
288
  const storage = await loadStorage(ctx, agentDir);
289
+ const artifacts = await loadArtifacts(ctx, agentDir);
290
+ await appendArtifactTool(ctx, agentDir, agent, artifacts);
283
291
 
284
292
  if (agentDir !== rootDir) {
285
293
  await warnUnknownSlots(ctx, agentDir);
@@ -297,6 +305,7 @@ export async function loadAgentProject(
297
305
  abs,
298
306
  abConfig,
299
307
  storage,
308
+ artifacts,
300
309
  diagnostics,
301
310
  };
302
311
  }
@@ -351,6 +360,14 @@ async function loadAgent(
351
360
  message: "builtinTools is not supported on subagents; ignoring.",
352
361
  });
353
362
  }
363
+ const artifactsPath = await findModuleFile(agentDir, "artifacts");
364
+ if (artifactsPath !== undefined) {
365
+ ctx.diagnostics.push({
366
+ severity: "warning",
367
+ path: displayPath(ctx, artifactsPath),
368
+ message: "artifacts are not supported on subagents; ignoring.",
369
+ });
370
+ }
354
371
  return {
355
372
  name,
356
373
  ...(config?.description === undefined
@@ -1579,6 +1596,81 @@ async function loadStorage(
1579
1596
  return definition as StorageDefinition | undefined;
1580
1597
  }
1581
1598
 
1599
+ // ============================================================================
1600
+ // Artifacts (agent/artifacts.ts)
1601
+ // ============================================================================
1602
+
1603
+ async function loadArtifacts(
1604
+ ctx: DiscoveryContext,
1605
+ agentDir: string
1606
+ ): Promise<ArtifactsDefinition | undefined> {
1607
+ const path = await findModuleFile(agentDir, "artifacts");
1608
+ if (path === undefined) {
1609
+ return undefined;
1610
+ }
1611
+ const imported = await importDefault(ctx, path, "artifacts");
1612
+ if (imported === undefined) {
1613
+ return undefined;
1614
+ }
1615
+ const definition = imported as ArtifactsDefinition;
1616
+ if (
1617
+ definition.agentTool === true &&
1618
+ Object.keys(definition.kinds ?? {}).length === 0
1619
+ ) {
1620
+ ctx.diagnostics.push({
1621
+ severity: "error",
1622
+ path: displayPath(ctx, path),
1623
+ message:
1624
+ "artifacts.agentTool requires kinds: the tag_artifact tool needs at least one declared kind; its descriptions are the tool prompt.",
1625
+ });
1626
+ }
1627
+ return definition;
1628
+ }
1629
+
1630
+ /**
1631
+ * Materialize the `tag_artifact` built-in when `defineArtifacts` set
1632
+ * `agentTool: true`. Appended after `loadAgent` because the tool is
1633
+ * generated from the artifacts kinds registry, not `AgentConfig.builtinTools`.
1634
+ */
1635
+ async function appendArtifactTool(
1636
+ ctx: DiscoveryContext,
1637
+ agentDir: string,
1638
+ agent: ResolvedAgent,
1639
+ artifacts: ArtifactsDefinition | undefined
1640
+ ): Promise<void> {
1641
+ const tool = artifactBuiltinTool(artifacts);
1642
+ if (tool === undefined) {
1643
+ return;
1644
+ }
1645
+ const artifactsPath = displayPath(
1646
+ ctx,
1647
+ (await findModuleFile(agentDir, "artifacts")) ??
1648
+ join(agentDir, "artifacts.ts")
1649
+ );
1650
+ // TODO(artifacts-cloud): on cloud runtimes, ship tag_artifact as an
1651
+ // execution: "agent" script and have the host fold its action.result
1652
+ // stream events into artifact records — agent-tool calls already surface
1653
+ // durably in the session stream.
1654
+ if (agent.runtime === "cloud") {
1655
+ ctx.diagnostics.push({
1656
+ severity: "warning",
1657
+ path: artifactsPath,
1658
+ message:
1659
+ "tag_artifact is an in-process server tool (like builtinTools) and is not available on cloud agents; it is skipped.",
1660
+ });
1661
+ return;
1662
+ }
1663
+ if (agent.tools.some((t) => t.name === TAG_ARTIFACT_TOOL_NAME)) {
1664
+ ctx.diagnostics.push({
1665
+ severity: "warning",
1666
+ path: artifactsPath,
1667
+ message: `Authored tool "${TAG_ARTIFACT_TOOL_NAME}" shadows the built-in tool of the same name; the authored tool wins.`,
1668
+ });
1669
+ return;
1670
+ }
1671
+ agent.tools.push(tool);
1672
+ }
1673
+
1582
1674
  function resolveABName(definition: unknown, fallback: string): string {
1583
1675
  if (
1584
1676
  typeof definition === "object" &&
@@ -1759,6 +1851,14 @@ export function projectInfo(project: AgentProject): AgentProjectInfo {
1759
1851
  project.storage === undefined
1760
1852
  ? undefined
1761
1853
  : { name: project.storage.name },
1854
+ artifacts:
1855
+ project.artifacts === undefined
1856
+ ? undefined
1857
+ : {
1858
+ kinds: Object.keys(project.artifacts.kinds ?? {}),
1859
+ agentTool: project.artifacts.agentTool === true,
1860
+ max: resolveArtifactsMax(project.artifacts),
1861
+ },
1762
1862
  diagnostics: project.diagnostics,
1763
1863
  };
1764
1864
  }
@@ -1779,6 +1879,7 @@ async function importDefault(
1779
1879
  | "hook"
1780
1880
  | "ab"
1781
1881
  | "storage"
1882
+ | "artifacts"
1782
1883
  ): Promise<unknown> {
1783
1884
  let mod: Record<string, unknown>;
1784
1885
  try {
@@ -107,6 +107,7 @@ export const AUTHORING_ENTRY_FILES: ReadonlyArray<
107
107
  ["evals/reporters", "evals/reporters.js"],
108
108
  ["evals/loaders", "evals/loaders.js"],
109
109
  ["ab", "ab.js"],
110
+ ["artifacts", "artifacts.js"],
110
111
  ["storage", "storage.js"],
111
112
  ["storage/file-kv", "storage-backends/file-kv.js"],
112
113
  ["storage/postgres-kv", "storage-backends/postgres-kv.js"],
@@ -115,6 +115,12 @@ function handlerArgs(send: SendMessageFn): ChannelHandlerArgs {
115
115
  delete: async () => {},
116
116
  },
117
117
  },
118
+ artifacts: {
119
+ tag: async () => {
120
+ throw new Error("artifacts not configured in test");
121
+ },
122
+ list: async () => [],
123
+ },
118
124
  send,
119
125
  getSession: async () => null,
120
126
  receive: async () => fakeSession("recv"),