@agent-native/dispatch 0.24.5 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/dist/actions/connect-external-agent.d.ts +18 -0
- package/dist/actions/connect-external-agent.d.ts.map +1 -0
- package/dist/actions/connect-external-agent.js +68 -0
- package/dist/actions/connect-external-agent.js.map +1 -0
- package/dist/actions/create-workspace-resource.d.ts +14 -14
- package/dist/actions/create-workspace-resource.js +9 -2
- package/dist/actions/create-workspace-resource.js.map +1 -1
- package/dist/actions/create_workspace_app_embed_session.d.ts +13 -0
- package/dist/actions/create_workspace_app_embed_session.d.ts.map +1 -0
- package/dist/actions/create_workspace_app_embed_session.js +30 -0
- package/dist/actions/create_workspace_app_embed_session.js.map +1 -0
- package/dist/actions/import-agent-pack.d.ts +96 -0
- package/dist/actions/import-agent-pack.d.ts.map +1 -0
- package/dist/actions/import-agent-pack.js +116 -0
- package/dist/actions/import-agent-pack.js.map +1 -0
- package/dist/actions/import-agent.d.ts +60 -0
- package/dist/actions/import-agent.d.ts.map +1 -0
- package/dist/actions/import-agent.js +82 -0
- package/dist/actions/import-agent.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +10 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-agent-pack.d.ts +36 -0
- package/dist/actions/list-agent-pack.d.ts.map +1 -0
- package/dist/actions/list-agent-pack.js +32 -0
- package/dist/actions/list-agent-pack.js.map +1 -0
- package/dist/actions/list-workspace-resource-options.d.ts +1 -1
- package/dist/actions/list-workspace-resource-options.js +8 -1
- package/dist/actions/list-workspace-resource-options.js.map +1 -1
- package/dist/actions/list-workspace-resources.d.ts +1 -1
- package/dist/actions/list-workspace-resources.js +8 -1
- package/dist/actions/list-workspace-resources.js.map +1 -1
- package/dist/actions/navigate.js +1 -1
- package/dist/actions/navigate.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +10 -10
- package/dist/actions/update-workspace-resource.d.ts +13 -13
- package/dist/actions/view-screen.js +18 -1
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +1 -8
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +33 -106
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/simple-agents-panel.d.ts +9 -0
- package/dist/components/simple-agents-panel.d.ts.map +1 -0
- package/dist/components/simple-agents-panel.js +405 -0
- package/dist/components/simple-agents-panel.js.map +1 -0
- package/dist/components/workspace-app-host.d.ts +29 -0
- package/dist/components/workspace-app-host.d.ts.map +1 -0
- package/dist/components/workspace-app-host.js +145 -0
- package/dist/components/workspace-app-host.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -4
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +25 -0
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/schema.js +1 -1
- package/dist/db/schema.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +1 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +10 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/agent-pack.d.ts +31 -0
- package/dist/lib/agent-pack.d.ts.map +1 -0
- package/dist/lib/agent-pack.js +149 -0
- package/dist/lib/agent-pack.js.map +1 -0
- package/dist/lib/simple-agent-profile.d.ts +36 -0
- package/dist/lib/simple-agent-profile.d.ts.map +1 -0
- package/dist/lib/simple-agent-profile.js +183 -0
- package/dist/lib/simple-agent-profile.js.map +1 -0
- package/dist/routes/pages/agents.d.ts.map +1 -1
- package/dist/routes/pages/agents.js +11 -1
- package/dist/routes/pages/agents.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +4 -95
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/chat.d.ts.map +1 -1
- package/dist/routes/pages/chat.js +62 -10
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/server/lib/agent-pack-store.d.ts +24 -0
- package/dist/server/lib/agent-pack-store.d.ts.map +1 -0
- package/dist/server/lib/agent-pack-store.js +22 -0
- package/dist/server/lib/agent-pack-store.js.map +1 -0
- package/dist/server/lib/dispatch-store.d.ts.map +1 -1
- package/dist/server/lib/dispatch-store.js +4 -0
- package/dist/server/lib/dispatch-store.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts +11 -0
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +155 -9
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/dist/server/lib/workspace-resources-store.d.ts +15 -1
- package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
- package/dist/server/lib/workspace-resources-store.js +11 -2
- package/dist/server/lib/workspace-resources-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +13 -0
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/shared/feature-flags.d.ts +7 -0
- package/dist/shared/feature-flags.d.ts.map +1 -0
- package/dist/shared/feature-flags.js +12 -0
- package/dist/shared/feature-flags.js.map +1 -0
- package/dist/shared/workspace-sso.d.ts +36 -0
- package/dist/shared/workspace-sso.d.ts.map +1 -0
- package/dist/shared/workspace-sso.js +99 -0
- package/dist/shared/workspace-sso.js.map +1 -0
- package/package.json +5 -3
- package/src/actions/connect-external-agent.ts +92 -0
- package/src/actions/create-workspace-resource.ts +9 -2
- package/src/actions/create_workspace_app_embed_session.ts +32 -0
- package/src/actions/import-agent-pack.ts +146 -0
- package/src/actions/import-agent.ts +102 -0
- package/src/actions/index.spec.ts +13 -0
- package/src/actions/index.ts +10 -0
- package/src/actions/list-agent-pack.ts +43 -0
- package/src/actions/list-workspace-resource-options.ts +8 -1
- package/src/actions/list-workspace-resources.ts +8 -1
- package/src/actions/navigate.ts +1 -1
- package/src/actions/view-screen.spec.ts +29 -1
- package/src/actions/view-screen.ts +19 -1
- package/src/components/app-icon.spec.tsx +10 -0
- package/src/components/index.ts +1 -0
- package/src/components/layout/Layout.spec.tsx +2 -0
- package/src/components/layout/Layout.tsx +59 -171
- package/src/components/simple-agents-panel.spec.tsx +73 -0
- package/src/components/simple-agents-panel.tsx +1226 -0
- package/src/components/workspace-app-host.spec.tsx +204 -0
- package/src/components/workspace-app-host.tsx +359 -0
- package/src/db/migrations.spec.ts +5 -1
- package/src/db/migrations.ts +28 -1
- package/src/db/schema.ts +1 -1
- package/src/hooks/use-navigation-state.spec.ts +25 -0
- package/src/hooks/use-navigation-state.ts +11 -0
- package/src/lib/agent-pack.spec.ts +47 -0
- package/src/lib/agent-pack.ts +226 -0
- package/src/lib/simple-agent-profile.spec.ts +77 -0
- package/src/lib/simple-agent-profile.ts +247 -0
- package/src/routes/index.spec.ts +1 -0
- package/src/routes/pages/agents.tsx +21 -1
- package/src/routes/pages/apps.$appId.tsx +3 -240
- package/src/routes/pages/chat.spec.tsx +62 -0
- package/src/routes/pages/chat.tsx +117 -9
- package/src/server/lib/agent-pack-store.ts +35 -0
- package/src/server/lib/dispatch-store.ts +8 -0
- package/src/server/lib/mcp-gateway.spec.ts +141 -0
- package/src/server/lib/mcp-gateway.ts +227 -15
- package/src/server/lib/workspace-resources-store.ts +9 -2
- package/src/server/plugins/agent-chat.ts +13 -0
- package/src/shared/feature-flags.ts +13 -0
- package/src/shared/workspace-sso.ts +118 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { agentPackRoot, normalizeAgentPack } from "./agent-pack.js";
|
|
4
|
+
|
|
5
|
+
describe("agent packs", () => {
|
|
6
|
+
it("normalizes a folder into a profile, references, and skills", () => {
|
|
7
|
+
const pack = normalizeAgentPack([
|
|
8
|
+
{
|
|
9
|
+
path: "researcher/agent.md",
|
|
10
|
+
content:
|
|
11
|
+
"---\nname: Researcher\ndescription: Finds evidence\ntools: WebSearch, Bash\n---\n\n# Role\nFind evidence.",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
path: "researcher/context/glossary.md",
|
|
15
|
+
content: "# Glossary\n\nUse citizens instead of users.",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
path: "researcher/skills/interviews/SKILL.md",
|
|
19
|
+
content:
|
|
20
|
+
"---\nname: Interviews\ndescription: Run interviews\n---\n\n# Workflow\nAsk open questions.",
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
expect(pack.profile.name).toBe("Researcher");
|
|
25
|
+
expect(pack.files).toEqual([
|
|
26
|
+
expect.objectContaining({
|
|
27
|
+
path: "context/glossary.md",
|
|
28
|
+
kind: "agent-file",
|
|
29
|
+
}),
|
|
30
|
+
expect.objectContaining({
|
|
31
|
+
path: "skills/interviews/SKILL.md",
|
|
32
|
+
kind: "skill",
|
|
33
|
+
name: "Interviews",
|
|
34
|
+
}),
|
|
35
|
+
]);
|
|
36
|
+
expect(agentPackRoot(pack.profile.slug)).toBe("agents/researcher");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("rejects private paths and oversized packs", () => {
|
|
40
|
+
expect(() =>
|
|
41
|
+
normalizeAgentPack([
|
|
42
|
+
{ path: "agent.md", content: "# Agent" },
|
|
43
|
+
{ path: ".env", content: "SECRET=not-for-import" },
|
|
44
|
+
]),
|
|
45
|
+
).toThrow("ignored or private");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseFrontmatter,
|
|
3
|
+
parseSkillMetadata,
|
|
4
|
+
} from "@agent-native/core/resources/metadata";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
buildSimpleAgentContent,
|
|
8
|
+
normalizeImportedAgent,
|
|
9
|
+
slugifyAgentName,
|
|
10
|
+
type ImportedAgentProfile,
|
|
11
|
+
} from "./simple-agent-profile.js";
|
|
12
|
+
|
|
13
|
+
export const AGENT_PACK_MAX_FILES = 80;
|
|
14
|
+
export const AGENT_PACK_MAX_FILE_BYTES = 200_000;
|
|
15
|
+
export const AGENT_PACK_MAX_TOTAL_BYTES = 2_000_000;
|
|
16
|
+
|
|
17
|
+
const IGNORED_PACK_FILES = new Set([
|
|
18
|
+
".ds_store",
|
|
19
|
+
".git",
|
|
20
|
+
".gitignore",
|
|
21
|
+
".env",
|
|
22
|
+
".env.local",
|
|
23
|
+
"node_modules",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export interface AgentPackFileInput {
|
|
27
|
+
path: string;
|
|
28
|
+
content: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NormalizedAgentPackFile {
|
|
32
|
+
path: string;
|
|
33
|
+
content: string;
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
kind: "agent-file" | "skill";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface NormalizedAgentPack {
|
|
40
|
+
profile: ImportedAgentProfile;
|
|
41
|
+
profileFile: AgentPackFileInput;
|
|
42
|
+
files: NormalizedAgentPackFile[];
|
|
43
|
+
warnings: string[];
|
|
44
|
+
totalBytes: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizePackPath(value: string): string {
|
|
48
|
+
const normalized = value
|
|
49
|
+
.replaceAll("\\", "/")
|
|
50
|
+
.split("/")
|
|
51
|
+
.filter((part) => part && part !== ".")
|
|
52
|
+
.join("/")
|
|
53
|
+
.trim();
|
|
54
|
+
if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
|
|
55
|
+
throw new Error(`Invalid agent pack path: ${value}`);
|
|
56
|
+
}
|
|
57
|
+
if (normalized.length > 240) {
|
|
58
|
+
throw new Error(`Agent pack path is too long: ${value}`);
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function shouldIgnorePackPath(path: string): boolean {
|
|
64
|
+
return path
|
|
65
|
+
.split("/")
|
|
66
|
+
.some((part) => IGNORED_PACK_FILES.has(part.toLowerCase()));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function commonRoot(paths: string[]): string {
|
|
70
|
+
const firstParts = paths.map((path) => path.split("/")[0]);
|
|
71
|
+
if (
|
|
72
|
+
firstParts.length < 2 ||
|
|
73
|
+
!firstParts.every((part) => part === firstParts[0])
|
|
74
|
+
) {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
const root = firstParts[0];
|
|
78
|
+
if (
|
|
79
|
+
!root ||
|
|
80
|
+
["skills", "context", "references", "files", "scripts"].includes(root)
|
|
81
|
+
) {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
return root;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stripCommonRoot(files: AgentPackFileInput[]): AgentPackFileInput[] {
|
|
88
|
+
const root = commonRoot(files.map((file) => normalizePackPath(file.path)));
|
|
89
|
+
if (!root) return files;
|
|
90
|
+
return files.map((file) => ({
|
|
91
|
+
...file,
|
|
92
|
+
path: normalizePackPath(file.path).slice(root.length + 1),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function profileScore(file: AgentPackFileInput): number {
|
|
97
|
+
const name = file.path.split("/").pop()?.toLowerCase() || "";
|
|
98
|
+
if (["agent.md", "agent.json", "profile.md"].includes(name)) return 100;
|
|
99
|
+
if (["agents.md", "claude.md", "instructions.md"].includes(name)) return 90;
|
|
100
|
+
if (name.endsWith(".md")) return 50;
|
|
101
|
+
if (name.endsWith(".json")) return 40;
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fileDescription(file: AgentPackFileInput): string | undefined {
|
|
106
|
+
const frontmatter = parseFrontmatter(file.content);
|
|
107
|
+
const description = frontmatter?.fields.find(
|
|
108
|
+
(field) => field.key === "description",
|
|
109
|
+
)?.value;
|
|
110
|
+
if (description?.trim()) return description.trim();
|
|
111
|
+
const heading = file.content.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
112
|
+
return heading || undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function fileName(path: string): string {
|
|
116
|
+
return path.split("/").pop() || path;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isSkillFile(path: string): boolean {
|
|
120
|
+
return (
|
|
121
|
+
path.startsWith("skills/") &&
|
|
122
|
+
path.endsWith(".md") &&
|
|
123
|
+
(path.endsWith("/SKILL.md") || !path.slice("skills/".length).includes("/"))
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function normalizeAgentPack(
|
|
128
|
+
inputFiles: AgentPackFileInput[],
|
|
129
|
+
): NormalizedAgentPack {
|
|
130
|
+
if (inputFiles.length === 0) {
|
|
131
|
+
throw new Error("Choose an agent file or folder with at least one file.");
|
|
132
|
+
}
|
|
133
|
+
if (inputFiles.length > AGENT_PACK_MAX_FILES) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Agent packs can contain at most ${AGENT_PACK_MAX_FILES} files.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const files = stripCommonRoot(
|
|
140
|
+
inputFiles.map((file) => {
|
|
141
|
+
const path = normalizePackPath(file.path);
|
|
142
|
+
if (shouldIgnorePackPath(path)) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Remove ignored or private path from the pack: ${path}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (typeof file.content !== "string") {
|
|
148
|
+
throw new Error(`Agent pack file is not text: ${path}`);
|
|
149
|
+
}
|
|
150
|
+
const size = Buffer.byteLength(file.content, "utf8");
|
|
151
|
+
if (size > AGENT_PACK_MAX_FILE_BYTES) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`${path} is too large. Keep text files under ${Math.round(AGENT_PACK_MAX_FILE_BYTES / 1024)} KB.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return { path, content: file.content };
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const totalBytes = files.reduce(
|
|
161
|
+
(total, file) => total + Buffer.byteLength(file.content, "utf8"),
|
|
162
|
+
0,
|
|
163
|
+
);
|
|
164
|
+
if (totalBytes > AGENT_PACK_MAX_TOTAL_BYTES) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Agent packs can contain at most ${Math.round(AGENT_PACK_MAX_TOTAL_BYTES / 1_000_000)} MB of text.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const profileFile = [...files]
|
|
171
|
+
.filter((file) => profileScore(file) > 0)
|
|
172
|
+
.sort(
|
|
173
|
+
(a, b) =>
|
|
174
|
+
profileScore(b) - profileScore(a) || a.path.localeCompare(b.path),
|
|
175
|
+
)[0];
|
|
176
|
+
if (!profileFile) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
"An agent pack needs an agent.md, CLAUDE.md, or Markdown profile file.",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const profile = normalizeImportedAgent(profileFile.content, profileFile.path);
|
|
183
|
+
const warnings: string[] = [...profile.warnings];
|
|
184
|
+
const packFiles = files
|
|
185
|
+
.filter((file) => file !== profileFile)
|
|
186
|
+
.map((file): NormalizedAgentPackFile => {
|
|
187
|
+
const skill = isSkillFile(file.path)
|
|
188
|
+
? parseSkillMetadata(file.content, file.path)
|
|
189
|
+
: null;
|
|
190
|
+
return {
|
|
191
|
+
path: file.path,
|
|
192
|
+
content: file.content,
|
|
193
|
+
name: skill?.name || fileName(file.path),
|
|
194
|
+
description: skill?.description || fileDescription(file),
|
|
195
|
+
kind: skill ? "skill" : "agent-file",
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (packFiles.length === 0) {
|
|
200
|
+
warnings.push(
|
|
201
|
+
"This pack contains only the profile; add files under context/ or skills/ when needed.",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { profile, profileFile, files: packFiles, warnings, totalBytes };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function agentPackProfileContent(input: {
|
|
209
|
+
profile: ImportedAgentProfile;
|
|
210
|
+
tools?: string;
|
|
211
|
+
sourceHash?: string;
|
|
212
|
+
importedAt?: string;
|
|
213
|
+
}): string {
|
|
214
|
+
return buildSimpleAgentContent({
|
|
215
|
+
...input.profile,
|
|
216
|
+
tools: input.tools,
|
|
217
|
+
source: input.profile.source,
|
|
218
|
+
sourcePath: input.profile.sourcePath,
|
|
219
|
+
sourceHash: input.sourceHash,
|
|
220
|
+
importedAt: input.importedAt,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function agentPackRoot(agentNameOrSlug: string): string {
|
|
225
|
+
return `agents/${slugifyAgentName(agentNameOrSlug)}`;
|
|
226
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildSimpleAgentContent,
|
|
5
|
+
normalizeImportedAgent,
|
|
6
|
+
slugifyAgentName,
|
|
7
|
+
validateImportedAgentTools,
|
|
8
|
+
} from "./simple-agent-profile.js";
|
|
9
|
+
|
|
10
|
+
describe("simple agent profiles", () => {
|
|
11
|
+
it("normalizes a Claude-style Markdown agent", () => {
|
|
12
|
+
const result = normalizeImportedAgent(
|
|
13
|
+
`---\nname: Research Partner\ndescription: Synthesizes research\nmodel: sonnet\ntools: Read, WebSearch\n---\n\n# Role\n\nResearch carefully.`,
|
|
14
|
+
".claude/agents/research.md",
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
expect(result).toMatchObject({
|
|
18
|
+
name: "Research Partner",
|
|
19
|
+
description: "Synthesizes research",
|
|
20
|
+
model: "sonnet",
|
|
21
|
+
tools: "Read, WebSearch",
|
|
22
|
+
source: "claude",
|
|
23
|
+
sourcePath: ".claude/agents/research.md",
|
|
24
|
+
});
|
|
25
|
+
expect(result.instructions).toContain("Research carefully.");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("normalizes a generic JSON agent and reports unsafe fields", () => {
|
|
29
|
+
const result = normalizeImportedAgent(
|
|
30
|
+
JSON.stringify({
|
|
31
|
+
name: "Launch Reviewer",
|
|
32
|
+
systemPrompt: "Review launch plans.",
|
|
33
|
+
tools: ["Read", "Search"],
|
|
34
|
+
hooks: { afterRun: "rm -rf ./tmp" },
|
|
35
|
+
}),
|
|
36
|
+
"agent.json",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(result).toMatchObject({
|
|
40
|
+
name: "Launch Reviewer",
|
|
41
|
+
tools: "Read, Search",
|
|
42
|
+
source: "json",
|
|
43
|
+
});
|
|
44
|
+
expect(result.warnings).toContain("Skipped unsafe capability: hooks");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("builds the canonical runtime profile content", () => {
|
|
48
|
+
expect(
|
|
49
|
+
buildSimpleAgentContent({
|
|
50
|
+
name: "Research Partner",
|
|
51
|
+
description: "Synthesizes research",
|
|
52
|
+
instructions: "Ask for evidence before making claims.",
|
|
53
|
+
source: "claude",
|
|
54
|
+
sourcePath: ".claude/agents/research.md",
|
|
55
|
+
sourceHash: "abc123",
|
|
56
|
+
}),
|
|
57
|
+
).toContain("source-hash: abc123");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("keeps resolvable imported tools and warns for the rest", () => {
|
|
61
|
+
expect(
|
|
62
|
+
validateImportedAgentTools(
|
|
63
|
+
"Read, Bash, WebSearch",
|
|
64
|
+
new Set(["bash", "web-search"]),
|
|
65
|
+
),
|
|
66
|
+
).toEqual({
|
|
67
|
+
tools: "bash, web-search",
|
|
68
|
+
warnings: ["Skipped unmapped tool: Read"],
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("creates stable slugs for names", () => {
|
|
73
|
+
expect(slugifyAgentName(" User Research / KPMG ")).toBe(
|
|
74
|
+
"user-research-kpmg",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import {
|
|
2
|
+
frontmatterFieldsToObject,
|
|
3
|
+
parseFrontmatter,
|
|
4
|
+
serializeFrontmatter,
|
|
5
|
+
} from "@agent-native/core/resources/metadata";
|
|
6
|
+
|
|
7
|
+
export interface SimpleAgentProfileInput {
|
|
8
|
+
name: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
tools?: string;
|
|
12
|
+
instructions: string;
|
|
13
|
+
source?: string;
|
|
14
|
+
sourcePath?: string;
|
|
15
|
+
sourceHash?: string;
|
|
16
|
+
importedAt?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ImportedAgentProfile {
|
|
20
|
+
name: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
tools?: string;
|
|
24
|
+
instructions: string;
|
|
25
|
+
slug: string;
|
|
26
|
+
source: "markdown" | "claude" | "json";
|
|
27
|
+
sourcePath?: string;
|
|
28
|
+
warnings: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ImportedAgentToolValidation {
|
|
32
|
+
tools?: string;
|
|
33
|
+
warnings: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SUPPORTED_FIELDS = new Set([
|
|
37
|
+
"name",
|
|
38
|
+
"description",
|
|
39
|
+
"model",
|
|
40
|
+
"tools",
|
|
41
|
+
"color",
|
|
42
|
+
"delegate-default",
|
|
43
|
+
"source",
|
|
44
|
+
"source-path",
|
|
45
|
+
"source-hash",
|
|
46
|
+
"imported-at",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
export function slugifyAgentName(value: string): string {
|
|
50
|
+
return (
|
|
51
|
+
value
|
|
52
|
+
.trim()
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
55
|
+
.replace(/^-+|-+$/g, "") || "agent"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function deriveName(fileName?: string): string {
|
|
60
|
+
const baseName = fileName?.replaceAll("\\", "/").split("/").pop();
|
|
61
|
+
const name = baseName?.replace(/\.(md|markdown|json|txt)$/i, "");
|
|
62
|
+
return name?.trim() || "Imported agent";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function stringValue(value: unknown): string | undefined {
|
|
66
|
+
if (typeof value !== "string") return undefined;
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
return trimmed || undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function toolsValue(value: unknown): string | undefined {
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
const tools = value.filter(
|
|
74
|
+
(item): item is string => typeof item === "string",
|
|
75
|
+
);
|
|
76
|
+
return tools.length > 0 ? tools.join(", ") : undefined;
|
|
77
|
+
}
|
|
78
|
+
return stringValue(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeToolKey(value: string): string {
|
|
82
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function importedToolNames(value: string): string[] {
|
|
86
|
+
const trimmed = value.trim();
|
|
87
|
+
if (!trimmed || trimmed.toLowerCase() === "inherit") return [];
|
|
88
|
+
const unwrapped = trimmed.replace(/^\[|\]$/g, "");
|
|
89
|
+
return unwrapped
|
|
90
|
+
.split(/[\n,]/)
|
|
91
|
+
.map((tool) => tool.trim().replace(/^['"]|['"]$/g, ""))
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Imported tool names are descriptive input, not a permission grant. Keep only
|
|
97
|
+
* names the current Dispatch surface can resolve and make every omission
|
|
98
|
+
* visible to the caller instead of persisting a misleading tool list.
|
|
99
|
+
*/
|
|
100
|
+
export function validateImportedAgentTools(
|
|
101
|
+
tools: string | undefined,
|
|
102
|
+
availableToolNames: ReadonlySet<string>,
|
|
103
|
+
): ImportedAgentToolValidation {
|
|
104
|
+
const names = importedToolNames(tools ?? "");
|
|
105
|
+
if (names.length === 0) return { warnings: [] };
|
|
106
|
+
|
|
107
|
+
const resolvable = new Map<string, string>();
|
|
108
|
+
for (const name of availableToolNames) {
|
|
109
|
+
resolvable.set(normalizeToolKey(name), name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const mapped: string[] = [];
|
|
113
|
+
const warnings: string[] = [];
|
|
114
|
+
for (const name of names) {
|
|
115
|
+
const resolved = resolvable.get(normalizeToolKey(name));
|
|
116
|
+
if (resolved) {
|
|
117
|
+
if (!mapped.includes(resolved)) mapped.push(resolved);
|
|
118
|
+
} else {
|
|
119
|
+
warnings.push(`Skipped unmapped tool: ${name}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
tools: mapped.length > 0 ? mapped.join(", ") : undefined,
|
|
125
|
+
warnings,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function markdownProfile(
|
|
130
|
+
source: string,
|
|
131
|
+
fileName?: string,
|
|
132
|
+
): Omit<ImportedAgentProfile, "source"> & {
|
|
133
|
+
source: "markdown" | "claude";
|
|
134
|
+
} {
|
|
135
|
+
const frontmatter = parseFrontmatter(source);
|
|
136
|
+
const values = frontmatterFieldsToObject(frontmatter);
|
|
137
|
+
const body = (frontmatter?.body ?? source).trim();
|
|
138
|
+
const heading = body.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
139
|
+
const name = stringValue(values.name) || heading || deriveName(fileName);
|
|
140
|
+
const warnings = Object.keys(values)
|
|
141
|
+
.filter((key) => !SUPPORTED_FIELDS.has(key))
|
|
142
|
+
.map((key) => `Ignored unsupported field: ${key}`);
|
|
143
|
+
if (!body) warnings.push("The imported agent has no instructions yet.");
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
name,
|
|
147
|
+
description: stringValue(values.description),
|
|
148
|
+
model: stringValue(values.model),
|
|
149
|
+
tools: toolsValue(values.tools),
|
|
150
|
+
instructions: body,
|
|
151
|
+
slug: slugifyAgentName(name),
|
|
152
|
+
source: fileName?.includes(".claude/") ? "claude" : "markdown",
|
|
153
|
+
sourcePath: fileName,
|
|
154
|
+
warnings,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function jsonProfile(source: string, fileName?: string): ImportedAgentProfile {
|
|
159
|
+
let parsed: unknown;
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(source);
|
|
162
|
+
} catch {
|
|
163
|
+
throw new Error("This file looks like JSON, but it is not valid JSON.");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const record =
|
|
167
|
+
parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
168
|
+
? (parsed as Record<string, unknown>)
|
|
169
|
+
: null;
|
|
170
|
+
const candidate =
|
|
171
|
+
record?.agent &&
|
|
172
|
+
typeof record.agent === "object" &&
|
|
173
|
+
!Array.isArray(record.agent)
|
|
174
|
+
? (record.agent as Record<string, unknown>)
|
|
175
|
+
: record;
|
|
176
|
+
if (!candidate) {
|
|
177
|
+
throw new Error("Import a single agent definition, not a JSON list.");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const name =
|
|
181
|
+
stringValue(candidate.name) ||
|
|
182
|
+
stringValue(candidate.title) ||
|
|
183
|
+
deriveName(fileName);
|
|
184
|
+
const instructions =
|
|
185
|
+
stringValue(candidate.instructions) ||
|
|
186
|
+
stringValue(candidate.systemPrompt) ||
|
|
187
|
+
stringValue(candidate.prompt) ||
|
|
188
|
+
stringValue(candidate.content) ||
|
|
189
|
+
"";
|
|
190
|
+
const warnings: string[] = [];
|
|
191
|
+
for (const key of ["hooks", "env", "environment", "command", "args", "cwd"]) {
|
|
192
|
+
if (candidate[key] !== undefined) {
|
|
193
|
+
warnings.push(`Skipped unsafe capability: ${key}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!instructions)
|
|
197
|
+
warnings.push("The imported agent has no instructions yet.");
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
name,
|
|
201
|
+
description: stringValue(candidate.description),
|
|
202
|
+
model: stringValue(candidate.model),
|
|
203
|
+
tools: toolsValue(candidate.tools),
|
|
204
|
+
instructions,
|
|
205
|
+
slug: slugifyAgentName(name),
|
|
206
|
+
source: "json",
|
|
207
|
+
sourcePath: fileName,
|
|
208
|
+
warnings,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function normalizeImportedAgent(
|
|
213
|
+
source: string,
|
|
214
|
+
fileName?: string,
|
|
215
|
+
): ImportedAgentProfile {
|
|
216
|
+
const trimmed = source.trim();
|
|
217
|
+
if (!trimmed) throw new Error("Paste an agent definition or choose a file.");
|
|
218
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
219
|
+
return jsonProfile(trimmed, fileName);
|
|
220
|
+
}
|
|
221
|
+
return markdownProfile(trimmed, fileName);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function buildSimpleAgentContent(
|
|
225
|
+
input: SimpleAgentProfileInput,
|
|
226
|
+
): string {
|
|
227
|
+
const fields = [
|
|
228
|
+
{ key: "name", value: input.name.trim() },
|
|
229
|
+
...(input.description?.trim()
|
|
230
|
+
? [{ key: "description", value: input.description.trim() }]
|
|
231
|
+
: []),
|
|
232
|
+
{ key: "model", value: input.model?.trim() || "inherit" },
|
|
233
|
+
{ key: "tools", value: input.tools?.trim() || "inherit" },
|
|
234
|
+
{ key: "delegate-default", value: "false" },
|
|
235
|
+
...(input.source ? [{ key: "source", value: input.source }] : []),
|
|
236
|
+
...(input.sourcePath
|
|
237
|
+
? [{ key: "source-path", value: input.sourcePath }]
|
|
238
|
+
: []),
|
|
239
|
+
...(input.sourceHash
|
|
240
|
+
? [{ key: "source-hash", value: input.sourceHash }]
|
|
241
|
+
: []),
|
|
242
|
+
...(input.importedAt
|
|
243
|
+
? [{ key: "imported-at", value: input.importedAt }]
|
|
244
|
+
: []),
|
|
245
|
+
];
|
|
246
|
+
return `${serializeFrontmatter(fields)}${input.instructions.trim()}\n`;
|
|
247
|
+
}
|
package/src/routes/index.spec.ts
CHANGED
|
@@ -18,6 +18,7 @@ describe("Dispatch route registration", () => {
|
|
|
18
18
|
expect(paths).toContain("browser-connect");
|
|
19
19
|
expect(paths).toContain("operations");
|
|
20
20
|
expect(paths).toContain("admin");
|
|
21
|
+
expect(paths).toContain("agents");
|
|
21
22
|
expect(paths.indexOf("chat")).toBeLessThan(paths.indexOf(":appId"));
|
|
22
23
|
expect(paths.indexOf("chat/:threadId")).toBeLessThan(
|
|
23
24
|
paths.indexOf(":appId"),
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import { useT } from "@agent-native/core/client/i18n";
|
|
6
6
|
import { IconCheck, IconCopy, IconPlugConnected } from "@tabler/icons-react";
|
|
7
7
|
import { useMemo, useState } from "react";
|
|
8
|
+
import { useLocation } from "react-router";
|
|
8
9
|
import { toast } from "sonner";
|
|
9
10
|
|
|
10
11
|
import { ActionQueryError } from "../../components/action-query-error";
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
type ConnectedAgent,
|
|
14
15
|
} from "../../components/agents-panel";
|
|
15
16
|
import { DispatchShell } from "../../components/dispatch-shell";
|
|
17
|
+
import { SimpleAgentsPanel } from "../../components/simple-agents-panel";
|
|
16
18
|
import { Button } from "../../components/ui/button";
|
|
17
19
|
import { Input } from "../../components/ui/input";
|
|
18
20
|
import { Skeleton } from "../../components/ui/skeleton";
|
|
@@ -203,7 +205,7 @@ function DispatchMcpAccessPanel() {
|
|
|
203
205
|
);
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
|
|
208
|
+
function ConnectedAgentsRoute() {
|
|
207
209
|
const t = useT();
|
|
208
210
|
const agentsQuery = useActionQuery("list-connected-agents", {});
|
|
209
211
|
|
|
@@ -229,3 +231,21 @@ export default function AgentsRoute() {
|
|
|
229
231
|
</DispatchShell>
|
|
230
232
|
);
|
|
231
233
|
}
|
|
234
|
+
|
|
235
|
+
export default function AgentsRoute() {
|
|
236
|
+
const t = useT();
|
|
237
|
+
const location = useLocation();
|
|
238
|
+
|
|
239
|
+
if (!location.pathname.startsWith("/admin/agents")) {
|
|
240
|
+
return (
|
|
241
|
+
<DispatchShell
|
|
242
|
+
title={t("dispatch.nav.agents")}
|
|
243
|
+
description={t("dispatch.pages.simpleAgentsDescription")}
|
|
244
|
+
>
|
|
245
|
+
<SimpleAgentsPanel />
|
|
246
|
+
</DispatchShell>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return <ConnectedAgentsRoute />;
|
|
251
|
+
}
|