@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.
Files changed (151) hide show
  1. package/README.md +12 -1
  2. package/dist/actions/connect-external-agent.d.ts +18 -0
  3. package/dist/actions/connect-external-agent.d.ts.map +1 -0
  4. package/dist/actions/connect-external-agent.js +68 -0
  5. package/dist/actions/connect-external-agent.js.map +1 -0
  6. package/dist/actions/create-workspace-resource.d.ts +14 -14
  7. package/dist/actions/create-workspace-resource.js +9 -2
  8. package/dist/actions/create-workspace-resource.js.map +1 -1
  9. package/dist/actions/create_workspace_app_embed_session.d.ts +13 -0
  10. package/dist/actions/create_workspace_app_embed_session.d.ts.map +1 -0
  11. package/dist/actions/create_workspace_app_embed_session.js +30 -0
  12. package/dist/actions/create_workspace_app_embed_session.js.map +1 -0
  13. package/dist/actions/import-agent-pack.d.ts +96 -0
  14. package/dist/actions/import-agent-pack.d.ts.map +1 -0
  15. package/dist/actions/import-agent-pack.js +116 -0
  16. package/dist/actions/import-agent-pack.js.map +1 -0
  17. package/dist/actions/import-agent.d.ts +60 -0
  18. package/dist/actions/import-agent.d.ts.map +1 -0
  19. package/dist/actions/import-agent.js +82 -0
  20. package/dist/actions/import-agent.js.map +1 -0
  21. package/dist/actions/index.d.ts.map +1 -1
  22. package/dist/actions/index.js +10 -0
  23. package/dist/actions/index.js.map +1 -1
  24. package/dist/actions/list-agent-pack.d.ts +36 -0
  25. package/dist/actions/list-agent-pack.d.ts.map +1 -0
  26. package/dist/actions/list-agent-pack.js +32 -0
  27. package/dist/actions/list-agent-pack.js.map +1 -0
  28. package/dist/actions/list-workspace-resource-options.d.ts +1 -1
  29. package/dist/actions/list-workspace-resource-options.js +8 -1
  30. package/dist/actions/list-workspace-resource-options.js.map +1 -1
  31. package/dist/actions/list-workspace-resources.d.ts +1 -1
  32. package/dist/actions/list-workspace-resources.js +8 -1
  33. package/dist/actions/list-workspace-resources.js.map +1 -1
  34. package/dist/actions/navigate.js +1 -1
  35. package/dist/actions/navigate.js.map +1 -1
  36. package/dist/actions/provider-api-register.d.ts +10 -10
  37. package/dist/actions/update-workspace-resource.d.ts +13 -13
  38. package/dist/actions/view-screen.js +18 -1
  39. package/dist/actions/view-screen.js.map +1 -1
  40. package/dist/components/index.d.ts +1 -0
  41. package/dist/components/index.d.ts.map +1 -1
  42. package/dist/components/index.js +1 -0
  43. package/dist/components/index.js.map +1 -1
  44. package/dist/components/layout/Layout.d.ts +1 -8
  45. package/dist/components/layout/Layout.d.ts.map +1 -1
  46. package/dist/components/layout/Layout.js +33 -106
  47. package/dist/components/layout/Layout.js.map +1 -1
  48. package/dist/components/simple-agents-panel.d.ts +9 -0
  49. package/dist/components/simple-agents-panel.d.ts.map +1 -0
  50. package/dist/components/simple-agents-panel.js +405 -0
  51. package/dist/components/simple-agents-panel.js.map +1 -0
  52. package/dist/components/workspace-app-host.d.ts +29 -0
  53. package/dist/components/workspace-app-host.d.ts.map +1 -0
  54. package/dist/components/workspace-app-host.js +145 -0
  55. package/dist/components/workspace-app-host.js.map +1 -0
  56. package/dist/db/migrations.d.ts +2 -4
  57. package/dist/db/migrations.d.ts.map +1 -1
  58. package/dist/db/migrations.js +25 -0
  59. package/dist/db/migrations.js.map +1 -1
  60. package/dist/db/schema.js +1 -1
  61. package/dist/db/schema.js.map +1 -1
  62. package/dist/hooks/use-navigation-state.d.ts +1 -0
  63. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  64. package/dist/hooks/use-navigation-state.js +10 -0
  65. package/dist/hooks/use-navigation-state.js.map +1 -1
  66. package/dist/lib/agent-pack.d.ts +31 -0
  67. package/dist/lib/agent-pack.d.ts.map +1 -0
  68. package/dist/lib/agent-pack.js +149 -0
  69. package/dist/lib/agent-pack.js.map +1 -0
  70. package/dist/lib/simple-agent-profile.d.ts +36 -0
  71. package/dist/lib/simple-agent-profile.d.ts.map +1 -0
  72. package/dist/lib/simple-agent-profile.js +183 -0
  73. package/dist/lib/simple-agent-profile.js.map +1 -0
  74. package/dist/routes/pages/agents.d.ts.map +1 -1
  75. package/dist/routes/pages/agents.js +11 -1
  76. package/dist/routes/pages/agents.js.map +1 -1
  77. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  78. package/dist/routes/pages/apps.$appId.js +4 -95
  79. package/dist/routes/pages/apps.$appId.js.map +1 -1
  80. package/dist/routes/pages/chat.d.ts.map +1 -1
  81. package/dist/routes/pages/chat.js +62 -10
  82. package/dist/routes/pages/chat.js.map +1 -1
  83. package/dist/server/lib/agent-pack-store.d.ts +24 -0
  84. package/dist/server/lib/agent-pack-store.d.ts.map +1 -0
  85. package/dist/server/lib/agent-pack-store.js +22 -0
  86. package/dist/server/lib/agent-pack-store.js.map +1 -0
  87. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  88. package/dist/server/lib/dispatch-store.js +4 -0
  89. package/dist/server/lib/dispatch-store.js.map +1 -1
  90. package/dist/server/lib/mcp-gateway.d.ts +11 -0
  91. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  92. package/dist/server/lib/mcp-gateway.js +155 -9
  93. package/dist/server/lib/mcp-gateway.js.map +1 -1
  94. package/dist/server/lib/workspace-resources-store.d.ts +15 -1
  95. package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
  96. package/dist/server/lib/workspace-resources-store.js +11 -2
  97. package/dist/server/lib/workspace-resources-store.js.map +1 -1
  98. package/dist/server/plugins/agent-chat.js +13 -0
  99. package/dist/server/plugins/agent-chat.js.map +1 -1
  100. package/dist/shared/feature-flags.d.ts +7 -0
  101. package/dist/shared/feature-flags.d.ts.map +1 -0
  102. package/dist/shared/feature-flags.js +12 -0
  103. package/dist/shared/feature-flags.js.map +1 -0
  104. package/dist/shared/workspace-sso.d.ts +36 -0
  105. package/dist/shared/workspace-sso.d.ts.map +1 -0
  106. package/dist/shared/workspace-sso.js +99 -0
  107. package/dist/shared/workspace-sso.js.map +1 -0
  108. package/package.json +5 -3
  109. package/src/actions/connect-external-agent.ts +92 -0
  110. package/src/actions/create-workspace-resource.ts +9 -2
  111. package/src/actions/create_workspace_app_embed_session.ts +32 -0
  112. package/src/actions/import-agent-pack.ts +146 -0
  113. package/src/actions/import-agent.ts +102 -0
  114. package/src/actions/index.spec.ts +13 -0
  115. package/src/actions/index.ts +10 -0
  116. package/src/actions/list-agent-pack.ts +43 -0
  117. package/src/actions/list-workspace-resource-options.ts +8 -1
  118. package/src/actions/list-workspace-resources.ts +8 -1
  119. package/src/actions/navigate.ts +1 -1
  120. package/src/actions/view-screen.spec.ts +29 -1
  121. package/src/actions/view-screen.ts +19 -1
  122. package/src/components/app-icon.spec.tsx +10 -0
  123. package/src/components/index.ts +1 -0
  124. package/src/components/layout/Layout.spec.tsx +2 -0
  125. package/src/components/layout/Layout.tsx +59 -171
  126. package/src/components/simple-agents-panel.spec.tsx +73 -0
  127. package/src/components/simple-agents-panel.tsx +1226 -0
  128. package/src/components/workspace-app-host.spec.tsx +204 -0
  129. package/src/components/workspace-app-host.tsx +359 -0
  130. package/src/db/migrations.spec.ts +5 -1
  131. package/src/db/migrations.ts +28 -1
  132. package/src/db/schema.ts +1 -1
  133. package/src/hooks/use-navigation-state.spec.ts +25 -0
  134. package/src/hooks/use-navigation-state.ts +11 -0
  135. package/src/lib/agent-pack.spec.ts +47 -0
  136. package/src/lib/agent-pack.ts +226 -0
  137. package/src/lib/simple-agent-profile.spec.ts +77 -0
  138. package/src/lib/simple-agent-profile.ts +247 -0
  139. package/src/routes/index.spec.ts +1 -0
  140. package/src/routes/pages/agents.tsx +21 -1
  141. package/src/routes/pages/apps.$appId.tsx +3 -240
  142. package/src/routes/pages/chat.spec.tsx +62 -0
  143. package/src/routes/pages/chat.tsx +117 -9
  144. package/src/server/lib/agent-pack-store.ts +35 -0
  145. package/src/server/lib/dispatch-store.ts +8 -0
  146. package/src/server/lib/mcp-gateway.spec.ts +141 -0
  147. package/src/server/lib/mcp-gateway.ts +227 -15
  148. package/src/server/lib/workspace-resources-store.ts +9 -2
  149. package/src/server/plugins/agent-chat.ts +13 -0
  150. package/src/shared/feature-flags.ts +13 -0
  151. 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
+ }
@@ -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
- export default function AgentsRoute() {
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
+ }