@agent-native/core 0.18.0 → 0.19.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 +1 -11
- package/dist/a2a/client.d.ts +7 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +3 -0
- package/dist/a2a/client.js.map +1 -1
- package/dist/cli/connect.d.ts +94 -0
- package/dist/cli/connect.d.ts.map +1 -0
- package/dist/cli/connect.js +443 -0
- package/dist/cli/connect.js.map +1 -0
- package/dist/cli/index.js +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-config-writers.d.ts +71 -0
- package/dist/cli/mcp-config-writers.d.ts.map +1 -0
- package/dist/cli/mcp-config-writers.js +210 -0
- package/dist/cli/mcp-config-writers.js.map +1 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +11 -63
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts +6 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +5 -4
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +6 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +20 -10
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/conversation/AgentConversation.d.ts +18 -0
- package/dist/client/conversation/AgentConversation.d.ts.map +1 -0
- package/dist/client/conversation/AgentConversation.js +94 -0
- package/dist/client/conversation/AgentConversation.js.map +1 -0
- package/dist/client/conversation/AgentConversation.spec.d.ts +2 -0
- package/dist/client/conversation/AgentConversation.spec.d.ts.map +1 -0
- package/dist/client/conversation/AgentConversation.spec.js +69 -0
- package/dist/client/conversation/AgentConversation.spec.js.map +1 -0
- package/dist/client/conversation/index.d.ts +4 -0
- package/dist/client/conversation/index.d.ts.map +1 -0
- package/dist/client/conversation/index.js +3 -0
- package/dist/client/conversation/index.js.map +1 -0
- package/dist/client/conversation/types.d.ts +54 -0
- package/dist/client/conversation/types.d.ts.map +1 -0
- package/dist/client/conversation/types.js +2 -0
- package/dist/client/conversation/types.js.map +1 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +15 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.js +66 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/resources/ResourceTree.d.ts.map +1 -1
- package/dist/client/resources/ResourceTree.js +2 -2
- package/dist/client/resources/ResourceTree.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +4 -28
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/code-agents/index.d.ts +1 -0
- package/dist/code-agents/index.d.ts.map +1 -1
- package/dist/code-agents/index.js +1 -0
- package/dist/code-agents/index.js.map +1 -1
- package/dist/code-agents/transcript-normalizer.d.ts +50 -0
- package/dist/code-agents/transcript-normalizer.d.ts.map +1 -0
- package/dist/code-agents/transcript-normalizer.js +356 -0
- package/dist/code-agents/transcript-normalizer.js.map +1 -0
- package/dist/extensions/schema.d.ts +1 -1
- package/dist/mcp/build-server.d.ts +20 -3
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +120 -15
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts +8 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +115 -13
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/mcp/connect-route.d.ts +43 -0
- package/dist/mcp/connect-route.d.ts.map +1 -0
- package/dist/mcp/connect-route.js +638 -0
- package/dist/mcp/connect-route.js.map +1 -0
- package/dist/mcp/connect-store.d.ts +132 -0
- package/dist/mcp/connect-store.d.ts.map +1 -0
- package/dist/mcp/connect-store.js +434 -0
- package/dist/mcp/connect-store.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +23 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.d.ts.map +1 -1
- package/dist/mcp/stdio.js +1 -0
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +1 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts +17 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +192 -49
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts +43 -0
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +25 -0
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +12 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +42 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/identity-sso-store.d.ts +86 -0
- package/dist/server/identity-sso-store.d.ts.map +1 -0
- package/dist/server/identity-sso-store.js +243 -0
- package/dist/server/identity-sso-store.js.map +1 -0
- package/dist/server/identity-sso.d.ts +78 -0
- package/dist/server/identity-sso.d.ts.map +1 -0
- package/dist/server/identity-sso.js +425 -0
- package/dist/server/identity-sso.js.map +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +2 -1
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/server/open-route.d.ts.map +1 -1
- package/dist/server/open-route.js +36 -5
- package/dist/server/open-route.js.map +1 -1
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/sharing/schema.d.ts +1 -1
- package/docs/content/code-agents-ui.md +14 -3
- package/docs/content/cross-app-sso.md +118 -0
- package/docs/content/external-agents.md +130 -51
- package/docs/content/migration-workbench.md +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-native connect <url>` — wire your local Claude Code / Codex / Cowork
|
|
3
|
+
* to a DEPLOYED agent-native app using a browser device-code flow. No token
|
|
4
|
+
* copying: open the verification URL, approve in the browser, and the minted
|
|
5
|
+
* HTTP MCP server entry is written into your client config(s) idempotently.
|
|
6
|
+
*
|
|
7
|
+
* agent-native connect <url> [--client all|claude-code|claude-code-cli|
|
|
8
|
+
* codex|cowork] [--scope user|project]
|
|
9
|
+
* [--name <serverName>]
|
|
10
|
+
* agent-native connect <url> --token <token> (no-browser fallback)
|
|
11
|
+
* agent-native connect --all [--client ...] (every first-party app)
|
|
12
|
+
*
|
|
13
|
+
* Server contract (implemented by another agent on `<url>`):
|
|
14
|
+
* POST <url>/_agent-native/mcp/connect/device/start (no auth)
|
|
15
|
+
* body { client?, app? }
|
|
16
|
+
* → { device_code, user_code, verification_uri,
|
|
17
|
+
* verification_uri_complete, interval, expires_in }
|
|
18
|
+
* POST <url>/_agent-native/mcp/connect/device/poll (no auth)
|
|
19
|
+
* body { device_code }
|
|
20
|
+
* → { status: "pending" }
|
|
21
|
+
* | { status: "approved", token, mcpUrl, serverName, mcpServerEntry }
|
|
22
|
+
* | { status: "expired" }
|
|
23
|
+
* | { status: "consumed" }
|
|
24
|
+
* | { status: "error" | "not_found", message? }
|
|
25
|
+
*
|
|
26
|
+
* Node-only CLI module. No new npm deps (Node built-ins + global fetch only).
|
|
27
|
+
*/
|
|
28
|
+
import { spawn } from "node:child_process";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { findWorkspaceRoot } from "../mcp/workspace-resolve.js";
|
|
31
|
+
import { CLIENTS, writeHttpEntryForClient, } from "./mcp-config-writers.js";
|
|
32
|
+
import { visibleTemplates } from "./templates-meta.js";
|
|
33
|
+
const DEVICE_START_PATH = "/_agent-native/mcp/connect/device/start";
|
|
34
|
+
const DEVICE_POLL_PATH = "/_agent-native/mcp/connect/device/poll";
|
|
35
|
+
const SERVER_NAME_PREFIX = "agent-native";
|
|
36
|
+
function logOut(msg) {
|
|
37
|
+
process.stdout.write(`${msg}\n`);
|
|
38
|
+
}
|
|
39
|
+
function logErr(msg) {
|
|
40
|
+
process.stderr.write(`${msg}\n`);
|
|
41
|
+
}
|
|
42
|
+
export function parseConnectArgs(argv) {
|
|
43
|
+
const out = {
|
|
44
|
+
client: "all",
|
|
45
|
+
scope: "user",
|
|
46
|
+
all: false,
|
|
47
|
+
};
|
|
48
|
+
for (let i = 0; i < argv.length; i++) {
|
|
49
|
+
const a = argv[i];
|
|
50
|
+
const eat = (flag) => {
|
|
51
|
+
if (a === flag)
|
|
52
|
+
return argv[++i];
|
|
53
|
+
if (a.startsWith(`${flag}=`))
|
|
54
|
+
return a.slice(flag.length + 1);
|
|
55
|
+
return undefined;
|
|
56
|
+
};
|
|
57
|
+
let v;
|
|
58
|
+
if (a === "--all")
|
|
59
|
+
out.all = true;
|
|
60
|
+
else if ((v = eat("--client")) !== undefined)
|
|
61
|
+
out.client = v;
|
|
62
|
+
else if ((v = eat("--scope")) !== undefined)
|
|
63
|
+
out.scope = v;
|
|
64
|
+
else if ((v = eat("--name")) !== undefined)
|
|
65
|
+
out.name = v;
|
|
66
|
+
else if ((v = eat("--token")) !== undefined)
|
|
67
|
+
out.token = v;
|
|
68
|
+
else if (!a.startsWith("-") && !out.url)
|
|
69
|
+
out.url = a;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Normalize a user-supplied app URL: trim, require http/https, strip the
|
|
75
|
+
* trailing slash. Throws a friendly Error otherwise.
|
|
76
|
+
*/
|
|
77
|
+
export function normalizeUrl(raw) {
|
|
78
|
+
const trimmed = (raw ?? "").trim();
|
|
79
|
+
if (!trimmed) {
|
|
80
|
+
throw new Error("Missing app URL. Usage: agent-native connect <url>");
|
|
81
|
+
}
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = new URL(trimmed);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new Error(`Not a valid URL: "${raw}". Pass a full origin, e.g. ` +
|
|
88
|
+
`agent-native connect https://mail.agent-native.com`);
|
|
89
|
+
}
|
|
90
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
91
|
+
throw new Error(`Unsupported URL scheme "${parsed.protocol}". Use http:// or https://`);
|
|
92
|
+
}
|
|
93
|
+
const host = parsed.hostname.toLowerCase();
|
|
94
|
+
const isLoopback = host === "localhost" ||
|
|
95
|
+
host === "127.0.0.1" ||
|
|
96
|
+
host === "::1" ||
|
|
97
|
+
host === "[::1]" ||
|
|
98
|
+
host.startsWith("127.");
|
|
99
|
+
if (parsed.protocol === "http:" && !isLoopback) {
|
|
100
|
+
throw new Error(`Refusing plaintext HTTP for non-loopback host "${parsed.hostname}". ` +
|
|
101
|
+
`Use https:// so bearer tokens are not sent in cleartext.`);
|
|
102
|
+
}
|
|
103
|
+
// origin + pathname, trailing slash stripped (origin keeps no path).
|
|
104
|
+
const base = `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "");
|
|
105
|
+
return base;
|
|
106
|
+
}
|
|
107
|
+
/** Resolve the requested clients list. "all" → every supported client. */
|
|
108
|
+
export function resolveClients(client) {
|
|
109
|
+
const c = (client ?? "all").toLowerCase();
|
|
110
|
+
if (c === "all" || c === "")
|
|
111
|
+
return [...CLIENTS];
|
|
112
|
+
if (CLIENTS.includes(c))
|
|
113
|
+
return [c];
|
|
114
|
+
throw new Error(`Unknown --client "${client}". Use: all, ${CLIENTS.join(", ")}`);
|
|
115
|
+
}
|
|
116
|
+
/** Derive an app slug from a deployed origin, e.g. mail.agent-native.com → mail. */
|
|
117
|
+
function appSlugFromUrl(url) {
|
|
118
|
+
try {
|
|
119
|
+
const host = new URL(url).hostname;
|
|
120
|
+
const first = host.split(".")[0];
|
|
121
|
+
return first && first !== "www" ? first : "app";
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return "app";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function defaultServerName(url) {
|
|
128
|
+
return `${SERVER_NAME_PREFIX}-${appSlugFromUrl(url)}`;
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Browser open (mirrors workspace-dev.ts openBrowser)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
function openInBrowser(url) {
|
|
134
|
+
if (process.env.AGENT_NATIVE_NO_OPEN === "1")
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
const command = process.platform === "darwin"
|
|
138
|
+
? "open"
|
|
139
|
+
: process.platform === "win32"
|
|
140
|
+
? "cmd"
|
|
141
|
+
: "xdg-open";
|
|
142
|
+
const openArgs = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
143
|
+
const child = spawn(command, openArgs, {
|
|
144
|
+
stdio: "ignore",
|
|
145
|
+
detached: true,
|
|
146
|
+
});
|
|
147
|
+
child.unref();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Non-fatal: the user can open the URL manually (we already printed it).
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function realSleep(ms) {
|
|
154
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
155
|
+
}
|
|
156
|
+
async function postJson(fetchImpl, url, body) {
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
159
|
+
try {
|
|
160
|
+
const response = await fetchImpl(url, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "content-type": "application/json" },
|
|
163
|
+
body: JSON.stringify(body ?? {}),
|
|
164
|
+
signal: controller.signal,
|
|
165
|
+
});
|
|
166
|
+
let json = null;
|
|
167
|
+
try {
|
|
168
|
+
json = await response.json();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
json = null;
|
|
172
|
+
}
|
|
173
|
+
return { status: response.status, json };
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
clearTimeout(timeout);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function responseMessage(json, fallback) {
|
|
180
|
+
const message = typeof json?.message === "string"
|
|
181
|
+
? json.message
|
|
182
|
+
: typeof json?.error === "string"
|
|
183
|
+
? json.error
|
|
184
|
+
: "";
|
|
185
|
+
return message.trim() || fallback;
|
|
186
|
+
}
|
|
187
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
188
|
+
/**
|
|
189
|
+
* Run the device-code flow against `baseUrl` and return the approved grant.
|
|
190
|
+
* Resolves with `null` (and prints a clear message) on expired/consumed or
|
|
191
|
+
* other terminal failure — the caller maps that to a non-zero exit.
|
|
192
|
+
*/
|
|
193
|
+
export async function runDeviceFlow(baseUrl, appSlug, clientArg, deps = {}) {
|
|
194
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
195
|
+
const sleep = deps.sleep ?? realSleep;
|
|
196
|
+
const open = deps.openBrowser ?? openInBrowser;
|
|
197
|
+
const now = deps.now ?? (() => Date.now());
|
|
198
|
+
let start;
|
|
199
|
+
try {
|
|
200
|
+
const { status, json } = await postJson(fetchImpl, `${baseUrl}${DEVICE_START_PATH}`, { client: clientArg, app: appSlug });
|
|
201
|
+
if (status < 200 || status >= 300 || !json?.device_code) {
|
|
202
|
+
logErr(` Could not start the connect flow on ${baseUrl} ` +
|
|
203
|
+
`(HTTP ${status}). Is this an agent-native app, and is it ` +
|
|
204
|
+
`deployed with the connect endpoint enabled?`);
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
start = json;
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
logErr(` Could not reach ${baseUrl} (${err?.message ?? err}). ` +
|
|
211
|
+
`Check the URL and your network.`);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const interval = Math.max(1, Number(start.interval) || 5);
|
|
215
|
+
const expiresIn = Math.max(interval, Number(start.expires_in) || 600);
|
|
216
|
+
const deadline = now() + expiresIn * 1000;
|
|
217
|
+
logOut("");
|
|
218
|
+
logOut(` Connecting to ${baseUrl}`);
|
|
219
|
+
logOut("");
|
|
220
|
+
logOut(` Your code: ${start.user_code}`);
|
|
221
|
+
logOut(` Open: ${start.verification_uri_complete}`);
|
|
222
|
+
logOut("");
|
|
223
|
+
logOut(" Approve in the browser to finish. Opening it now…");
|
|
224
|
+
open(start.verification_uri_complete);
|
|
225
|
+
let spin = 0;
|
|
226
|
+
const isTTY = !!process.stdout.isTTY;
|
|
227
|
+
while (now() < deadline) {
|
|
228
|
+
let poll;
|
|
229
|
+
try {
|
|
230
|
+
const { status, json } = await postJson(fetchImpl, `${baseUrl}${DEVICE_POLL_PATH}`, { device_code: start.device_code });
|
|
231
|
+
if (status < 200 || status >= 300) {
|
|
232
|
+
if (isTTY)
|
|
233
|
+
process.stdout.write("\r\x1b[K");
|
|
234
|
+
logErr(` Connect polling failed (HTTP ${status}): ` +
|
|
235
|
+
responseMessage(json, "server returned an error."));
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
poll = (json ?? { status: "pending" });
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Transient network error — keep polling until the deadline.
|
|
242
|
+
poll = { status: "pending" };
|
|
243
|
+
}
|
|
244
|
+
if (poll.status === "approved") {
|
|
245
|
+
if (isTTY)
|
|
246
|
+
process.stdout.write("\r\x1b[K");
|
|
247
|
+
const token = poll.token ?? "";
|
|
248
|
+
const mcpUrl = poll.mcpUrl ?? `${baseUrl}/_agent-native/mcp`;
|
|
249
|
+
const serverName = poll.serverName ?? `${SERVER_NAME_PREFIX}-${appSlug}`;
|
|
250
|
+
if (!token) {
|
|
251
|
+
logErr(" Server approved but returned no token. Aborting.");
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
logOut(" Approved.");
|
|
255
|
+
return { token, mcpUrl, serverName };
|
|
256
|
+
}
|
|
257
|
+
if (poll.status === "expired") {
|
|
258
|
+
if (isTTY)
|
|
259
|
+
process.stdout.write("\r\x1b[K");
|
|
260
|
+
logErr(" The connect request expired before it was approved.");
|
|
261
|
+
logErr(" Run the command again to retry.");
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
if (poll.status === "consumed") {
|
|
265
|
+
if (isTTY)
|
|
266
|
+
process.stdout.write("\r\x1b[K");
|
|
267
|
+
logErr(" This connect code was already used. Run the command again.");
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
if (poll.status === "error" || poll.status === "not_found") {
|
|
271
|
+
if (isTTY)
|
|
272
|
+
process.stdout.write("\r\x1b[K");
|
|
273
|
+
logErr(` Connect polling failed: ${responseMessage(poll, poll.status === "not_found"
|
|
274
|
+
? "device code was not found."
|
|
275
|
+
: "server returned an error.")}`);
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
if (isTTY) {
|
|
279
|
+
process.stdout.write(`\r ${SPINNER[spin++ % SPINNER.length]} Waiting for approval…`);
|
|
280
|
+
}
|
|
281
|
+
await sleep(interval * 1000);
|
|
282
|
+
}
|
|
283
|
+
if (isTTY)
|
|
284
|
+
process.stdout.write("\r\x1b[K");
|
|
285
|
+
logErr(" Timed out waiting for approval. Run the command again to retry.");
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Writing config(s)
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
function projectBaseDir() {
|
|
292
|
+
const cwd = process.cwd();
|
|
293
|
+
return findWorkspaceRoot(cwd) ?? path.resolve(cwd);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Write the HTTP MCP entry into every requested client config idempotently.
|
|
297
|
+
* Returns the list of files written so the caller can print them.
|
|
298
|
+
*/
|
|
299
|
+
export function writeConfigs(clients, serverName, mcpUrl, token, scope, baseDir = projectBaseDir()) {
|
|
300
|
+
const written = [];
|
|
301
|
+
for (const client of clients) {
|
|
302
|
+
const file = writeHttpEntryForClient(client, serverName, mcpUrl, token, baseDir, scope);
|
|
303
|
+
written.push({ client, file });
|
|
304
|
+
}
|
|
305
|
+
return written;
|
|
306
|
+
}
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Single-app connect
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
async function connectOne(rawUrl, parsed, deps) {
|
|
311
|
+
const baseUrl = normalizeUrl(rawUrl);
|
|
312
|
+
const appSlug = appSlugFromUrl(baseUrl);
|
|
313
|
+
const clients = resolveClients(parsed.client);
|
|
314
|
+
const scope = parsed.scope === "user" ? "user" : "project";
|
|
315
|
+
let token;
|
|
316
|
+
let mcpUrl;
|
|
317
|
+
let serverName;
|
|
318
|
+
if (parsed.token) {
|
|
319
|
+
// No-browser fallback: skip the device flow entirely.
|
|
320
|
+
token = parsed.token;
|
|
321
|
+
mcpUrl = `${baseUrl}/_agent-native/mcp`;
|
|
322
|
+
serverName = parsed.name ?? defaultServerName(baseUrl);
|
|
323
|
+
logOut("");
|
|
324
|
+
logOut(` Using supplied --token for ${baseUrl} (skipping browser flow).`);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
const grant = await runDeviceFlow(baseUrl, appSlug, parsed.client, deps);
|
|
328
|
+
if (!grant)
|
|
329
|
+
return { ok: false };
|
|
330
|
+
token = grant.token;
|
|
331
|
+
mcpUrl = grant.mcpUrl;
|
|
332
|
+
serverName = parsed.name ?? grant.serverName ?? defaultServerName(baseUrl);
|
|
333
|
+
}
|
|
334
|
+
const written = writeConfigs(clients, serverName, mcpUrl, token, scope);
|
|
335
|
+
logOut("");
|
|
336
|
+
logOut(` Connected "${serverName}" → ${mcpUrl}`);
|
|
337
|
+
for (const w of written) {
|
|
338
|
+
logOut(` ${w.client.padEnd(18)} ${w.file}`);
|
|
339
|
+
}
|
|
340
|
+
logOut("");
|
|
341
|
+
logOut(" Restart your coding agent to pick up the new MCP server.");
|
|
342
|
+
return { ok: true, serverName, files: written.map((w) => w.file) };
|
|
343
|
+
}
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// --all : connect every first-party hosted app
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
/** Hosted first-party apps: visible (non-hidden) templates with a prodUrl. */
|
|
348
|
+
export function hostedApps() {
|
|
349
|
+
return visibleTemplates()
|
|
350
|
+
.filter((t) => typeof t.prodUrl === "string" && t.prodUrl.length > 0)
|
|
351
|
+
.map((t) => ({ name: t.name, url: t.prodUrl }));
|
|
352
|
+
}
|
|
353
|
+
async function connectAll(parsed, deps) {
|
|
354
|
+
const apps = hostedApps();
|
|
355
|
+
if (apps.length === 0) {
|
|
356
|
+
logErr(" No hosted first-party apps found in the template registry.");
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
logOut("");
|
|
360
|
+
logOut(` Connecting ${apps.length} first-party hosted apps…`);
|
|
361
|
+
const results = [];
|
|
362
|
+
for (const app of apps) {
|
|
363
|
+
logOut("");
|
|
364
|
+
logOut(` ── ${app.name} (${app.url}) ──`);
|
|
365
|
+
try {
|
|
366
|
+
const res = await connectOne(app.url, parsed, deps);
|
|
367
|
+
results.push({
|
|
368
|
+
name: app.name,
|
|
369
|
+
status: res.ok ? "connected" : "skipped",
|
|
370
|
+
files: res.files ?? [],
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
logErr(` ${app.name}: ${err?.message ?? err}`);
|
|
375
|
+
results.push({ name: app.name, status: "error", files: [] });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
logOut("");
|
|
379
|
+
logOut(" Summary");
|
|
380
|
+
for (const r of results) {
|
|
381
|
+
const files = r.files.length ? r.files.join(", ") : "—";
|
|
382
|
+
logOut(` ${r.name.padEnd(14)} ${r.status.padEnd(10)} ${files}`);
|
|
383
|
+
}
|
|
384
|
+
return results.every((r) => r.status === "connected");
|
|
385
|
+
}
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// Entry point
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
const HELP = `agent-native connect — wire your coding agent to a deployed app
|
|
390
|
+
|
|
391
|
+
Usage:
|
|
392
|
+
agent-native connect <url> [--client <c>] [--scope user|project] [--name <n>]
|
|
393
|
+
Browser device-code flow. Prints a code, opens the verification URL,
|
|
394
|
+
polls until approved, then writes the HTTP MCP entry into your
|
|
395
|
+
client config(s). Idempotent — re-running replaces the same entry.
|
|
396
|
+
|
|
397
|
+
agent-native connect <url> --token <token>
|
|
398
|
+
No-browser fallback. Skip the device flow and write the entry with
|
|
399
|
+
the supplied token (get it from the app's Connect page).
|
|
400
|
+
|
|
401
|
+
agent-native connect --all [--client <c>] [--scope user|project]
|
|
402
|
+
Connect every first-party hosted app at once.
|
|
403
|
+
|
|
404
|
+
Clients: all (default), claude-code, claude-code-cli, codex, cowork
|
|
405
|
+
Scope: project (default, .mcp.json) or user (~/.claude.json)`;
|
|
406
|
+
/**
|
|
407
|
+
* `agent-native connect` entry point. `deps` is injectable for tests; the
|
|
408
|
+
* dispatcher in index.ts calls it with just `args`.
|
|
409
|
+
*
|
|
410
|
+
* Sets `process.exitCode = 1` on failure (so the process exits non-zero
|
|
411
|
+
* once the event loop drains) rather than calling `process.exit`, keeping
|
|
412
|
+
* the function testable — same pattern as `audit-agent-web`.
|
|
413
|
+
*/
|
|
414
|
+
export async function runConnect(args, deps = {}) {
|
|
415
|
+
if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
|
|
416
|
+
logOut(HELP);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const parsed = parseConnectArgs(args);
|
|
420
|
+
try {
|
|
421
|
+
if (parsed.all) {
|
|
422
|
+
const ok = await connectAll(parsed, deps);
|
|
423
|
+
if (!ok)
|
|
424
|
+
process.exitCode = 1;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (!parsed.url) {
|
|
428
|
+
logErr(" Missing app URL.");
|
|
429
|
+
logErr("");
|
|
430
|
+
logOut(HELP);
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const res = await connectOne(parsed.url, parsed, deps);
|
|
435
|
+
if (!res.ok)
|
|
436
|
+
process.exitCode = 1;
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
logErr(` ${err?.message ?? err}`);
|
|
440
|
+
process.exitCode = 1;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.js","sourceRoot":"","sources":["../../src/cli/connect.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `agent-native connect <url>` — wire your local Claude Code / Codex / Cowork\n * to a DEPLOYED agent-native app using a browser device-code flow. No token\n * copying: open the verification URL, approve in the browser, and the minted\n * HTTP MCP server entry is written into your client config(s) idempotently.\n *\n * agent-native connect <url> [--client all|claude-code|claude-code-cli|\n * codex|cowork] [--scope user|project]\n * [--name <serverName>]\n * agent-native connect <url> --token <token> (no-browser fallback)\n * agent-native connect --all [--client ...] (every first-party app)\n *\n * Server contract (implemented by another agent on `<url>`):\n * POST <url>/_agent-native/mcp/connect/device/start (no auth)\n * body { client?, app? }\n * → { device_code, user_code, verification_uri,\n * verification_uri_complete, interval, expires_in }\n * POST <url>/_agent-native/mcp/connect/device/poll (no auth)\n * body { device_code }\n * → { status: \"pending\" }\n * | { status: \"approved\", token, mcpUrl, serverName, mcpServerEntry }\n * | { status: \"expired\" }\n * | { status: \"consumed\" }\n * | { status: \"error\" | \"not_found\", message? }\n *\n * Node-only CLI module. No new npm deps (Node built-ins + global fetch only).\n */\n\nimport { spawn } from \"node:child_process\";\nimport path from \"node:path\";\n\nimport { findWorkspaceRoot } from \"../mcp/workspace-resolve.js\";\nimport {\n CLIENTS,\n ClientId,\n writeHttpEntryForClient,\n} from \"./mcp-config-writers.js\";\nimport { visibleTemplates } from \"./templates-meta.js\";\n\nconst DEVICE_START_PATH = \"/_agent-native/mcp/connect/device/start\";\nconst DEVICE_POLL_PATH = \"/_agent-native/mcp/connect/device/poll\";\nconst SERVER_NAME_PREFIX = \"agent-native\";\n\nfunction logOut(msg: string): void {\n process.stdout.write(`${msg}\\n`);\n}\nfunction logErr(msg: string): void {\n process.stderr.write(`${msg}\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// Arg parsing\n// ---------------------------------------------------------------------------\n\nexport interface ParsedConnectArgs {\n /** Positional URL (the deployed app origin). Undefined for `--all`. */\n url?: string;\n /** all | claude-code | claude-code-cli | codex | cowork (default \"all\"). */\n client: string;\n /** user | project (default \"user\"). */\n scope: string;\n /** Override the minted MCP server name. */\n name?: string;\n /** No-browser fallback: skip device flow, use this token directly. */\n token?: string;\n /** Connect every first-party hosted app. */\n all: boolean;\n}\n\nexport function parseConnectArgs(argv: string[]): ParsedConnectArgs {\n const out: ParsedConnectArgs = {\n client: \"all\",\n scope: \"user\",\n all: false,\n };\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n const eat = (flag: string): string | undefined => {\n if (a === flag) return argv[++i];\n if (a.startsWith(`${flag}=`)) return a.slice(flag.length + 1);\n return undefined;\n };\n let v: string | undefined;\n if (a === \"--all\") out.all = true;\n else if ((v = eat(\"--client\")) !== undefined) out.client = v;\n else if ((v = eat(\"--scope\")) !== undefined) out.scope = v;\n else if ((v = eat(\"--name\")) !== undefined) out.name = v;\n else if ((v = eat(\"--token\")) !== undefined) out.token = v;\n else if (!a.startsWith(\"-\") && !out.url) out.url = a;\n }\n return out;\n}\n\n/**\n * Normalize a user-supplied app URL: trim, require http/https, strip the\n * trailing slash. Throws a friendly Error otherwise.\n */\nexport function normalizeUrl(raw: string): string {\n const trimmed = (raw ?? \"\").trim();\n if (!trimmed) {\n throw new Error(\"Missing app URL. Usage: agent-native connect <url>\");\n }\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n throw new Error(\n `Not a valid URL: \"${raw}\". Pass a full origin, e.g. ` +\n `agent-native connect https://mail.agent-native.com`,\n );\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new Error(\n `Unsupported URL scheme \"${parsed.protocol}\". Use http:// or https://`,\n );\n }\n const host = parsed.hostname.toLowerCase();\n const isLoopback =\n host === \"localhost\" ||\n host === \"127.0.0.1\" ||\n host === \"::1\" ||\n host === \"[::1]\" ||\n host.startsWith(\"127.\");\n if (parsed.protocol === \"http:\" && !isLoopback) {\n throw new Error(\n `Refusing plaintext HTTP for non-loopback host \"${parsed.hostname}\". ` +\n `Use https:// so bearer tokens are not sent in cleartext.`,\n );\n }\n // origin + pathname, trailing slash stripped (origin keeps no path).\n const base = `${parsed.origin}${parsed.pathname}`.replace(/\\/+$/, \"\");\n return base;\n}\n\n/** Resolve the requested clients list. \"all\" → every supported client. */\nexport function resolveClients(client: string): ClientId[] {\n const c = (client ?? \"all\").toLowerCase();\n if (c === \"all\" || c === \"\") return [...CLIENTS];\n if ((CLIENTS as string[]).includes(c)) return [c as ClientId];\n throw new Error(\n `Unknown --client \"${client}\". Use: all, ${CLIENTS.join(\", \")}`,\n );\n}\n\n/** Derive an app slug from a deployed origin, e.g. mail.agent-native.com → mail. */\nfunction appSlugFromUrl(url: string): string {\n try {\n const host = new URL(url).hostname;\n const first = host.split(\".\")[0];\n return first && first !== \"www\" ? first : \"app\";\n } catch {\n return \"app\";\n }\n}\n\nfunction defaultServerName(url: string): string {\n return `${SERVER_NAME_PREFIX}-${appSlugFromUrl(url)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (mirrors workspace-dev.ts openBrowser)\n// ---------------------------------------------------------------------------\n\nfunction openInBrowser(url: string): void {\n if (process.env.AGENT_NATIVE_NO_OPEN === \"1\") return;\n try {\n const command =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"cmd\"\n : \"xdg-open\";\n const openArgs =\n process.platform === \"win32\" ? [\"/c\", \"start\", \"\", url] : [url];\n const child = spawn(command, openArgs, {\n stdio: \"ignore\",\n detached: true,\n });\n child.unref();\n } catch {\n // Non-fatal: the user can open the URL manually (we already printed it).\n }\n}\n\n// ---------------------------------------------------------------------------\n// Device-code flow\n// ---------------------------------------------------------------------------\n\ninterface DeviceStartResponse {\n device_code: string;\n user_code: string;\n verification_uri: string;\n verification_uri_complete: string;\n interval?: number;\n expires_in?: number;\n}\n\ninterface DevicePollResponse {\n status:\n | \"pending\"\n | \"approved\"\n | \"expired\"\n | \"consumed\"\n | \"error\"\n | \"not_found\";\n token?: string;\n mcpUrl?: string;\n serverName?: string;\n mcpServerEntry?: Record<string, unknown>;\n message?: string;\n error?: string;\n}\n\n/** Injectable hooks so the poll state machine is unit-testable. */\nexport interface ConnectDeps {\n /** Defaults to global fetch. */\n fetchImpl?: typeof fetch;\n /** Sleep between polls (ms). Defaults to real setTimeout. */\n sleep?: (ms: number) => Promise<void>;\n /** Open the verification URL. Defaults to the platform browser opener. */\n openBrowser?: (url: string) => void;\n /** Override \"now\" for the expiry cap (ms epoch). Defaults to Date.now. */\n now?: () => number;\n}\n\nfunction realSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function postJson(\n fetchImpl: typeof fetch,\n url: string,\n body: unknown,\n): Promise<{ status: number; json: any }> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 30_000);\n try {\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body ?? {}),\n signal: controller.signal,\n });\n let json: any = null;\n try {\n json = await response.json();\n } catch {\n json = null;\n }\n return { status: response.status, json };\n } finally {\n clearTimeout(timeout);\n }\n}\n\nfunction responseMessage(json: any, fallback: string): string {\n const message =\n typeof json?.message === \"string\"\n ? json.message\n : typeof json?.error === \"string\"\n ? json.error\n : \"\";\n return message.trim() || fallback;\n}\n\nconst SPINNER = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n\n/**\n * Run the device-code flow against `baseUrl` and return the approved grant.\n * Resolves with `null` (and prints a clear message) on expired/consumed or\n * other terminal failure — the caller maps that to a non-zero exit.\n */\nexport async function runDeviceFlow(\n baseUrl: string,\n appSlug: string,\n clientArg: string,\n deps: ConnectDeps = {},\n): Promise<{ token: string; mcpUrl: string; serverName: string } | null> {\n const fetchImpl = deps.fetchImpl ?? fetch;\n const sleep = deps.sleep ?? realSleep;\n const open = deps.openBrowser ?? openInBrowser;\n const now = deps.now ?? (() => Date.now());\n\n let start: DeviceStartResponse;\n try {\n const { status, json } = await postJson(\n fetchImpl,\n `${baseUrl}${DEVICE_START_PATH}`,\n { client: clientArg, app: appSlug },\n );\n if (status < 200 || status >= 300 || !json?.device_code) {\n logErr(\n ` Could not start the connect flow on ${baseUrl} ` +\n `(HTTP ${status}). Is this an agent-native app, and is it ` +\n `deployed with the connect endpoint enabled?`,\n );\n return null;\n }\n start = json as DeviceStartResponse;\n } catch (err: any) {\n logErr(\n ` Could not reach ${baseUrl} (${err?.message ?? err}). ` +\n `Check the URL and your network.`,\n );\n return null;\n }\n\n const interval = Math.max(1, Number(start.interval) || 5);\n const expiresIn = Math.max(interval, Number(start.expires_in) || 600);\n const deadline = now() + expiresIn * 1000;\n\n logOut(\"\");\n logOut(` Connecting to ${baseUrl}`);\n logOut(\"\");\n logOut(` Your code: ${start.user_code}`);\n logOut(` Open: ${start.verification_uri_complete}`);\n logOut(\"\");\n logOut(\" Approve in the browser to finish. Opening it now…\");\n open(start.verification_uri_complete);\n\n let spin = 0;\n const isTTY = !!process.stdout.isTTY;\n while (now() < deadline) {\n let poll: DevicePollResponse;\n try {\n const { status, json } = await postJson(\n fetchImpl,\n `${baseUrl}${DEVICE_POLL_PATH}`,\n { device_code: start.device_code },\n );\n if (status < 200 || status >= 300) {\n if (isTTY) process.stdout.write(\"\\r\\x1b[K\");\n logErr(\n ` Connect polling failed (HTTP ${status}): ` +\n responseMessage(json, \"server returned an error.\"),\n );\n return null;\n }\n poll = (json ?? { status: \"pending\" }) as DevicePollResponse;\n } catch {\n // Transient network error — keep polling until the deadline.\n poll = { status: \"pending\" };\n }\n\n if (poll.status === \"approved\") {\n if (isTTY) process.stdout.write(\"\\r\\x1b[K\");\n const token = poll.token ?? \"\";\n const mcpUrl = poll.mcpUrl ?? `${baseUrl}/_agent-native/mcp`;\n const serverName = poll.serverName ?? `${SERVER_NAME_PREFIX}-${appSlug}`;\n if (!token) {\n logErr(\" Server approved but returned no token. Aborting.\");\n return null;\n }\n logOut(\" Approved.\");\n return { token, mcpUrl, serverName };\n }\n if (poll.status === \"expired\") {\n if (isTTY) process.stdout.write(\"\\r\\x1b[K\");\n logErr(\" The connect request expired before it was approved.\");\n logErr(\" Run the command again to retry.\");\n return null;\n }\n if (poll.status === \"consumed\") {\n if (isTTY) process.stdout.write(\"\\r\\x1b[K\");\n logErr(\" This connect code was already used. Run the command again.\");\n return null;\n }\n if (poll.status === \"error\" || poll.status === \"not_found\") {\n if (isTTY) process.stdout.write(\"\\r\\x1b[K\");\n logErr(\n ` Connect polling failed: ${responseMessage(\n poll,\n poll.status === \"not_found\"\n ? \"device code was not found.\"\n : \"server returned an error.\",\n )}`,\n );\n return null;\n }\n\n if (isTTY) {\n process.stdout.write(\n `\\r ${SPINNER[spin++ % SPINNER.length]} Waiting for approval…`,\n );\n }\n await sleep(interval * 1000);\n }\n\n if (isTTY) process.stdout.write(\"\\r\\x1b[K\");\n logErr(\" Timed out waiting for approval. Run the command again to retry.\");\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Writing config(s)\n// ---------------------------------------------------------------------------\n\nfunction projectBaseDir(): string {\n const cwd = process.cwd();\n return findWorkspaceRoot(cwd) ?? path.resolve(cwd);\n}\n\n/**\n * Write the HTTP MCP entry into every requested client config idempotently.\n * Returns the list of files written so the caller can print them.\n */\nexport function writeConfigs(\n clients: ClientId[],\n serverName: string,\n mcpUrl: string,\n token: string,\n scope: string,\n baseDir: string = projectBaseDir(),\n): { client: ClientId; file: string }[] {\n const written: { client: ClientId; file: string }[] = [];\n for (const client of clients) {\n const file = writeHttpEntryForClient(\n client,\n serverName,\n mcpUrl,\n token,\n baseDir,\n scope,\n );\n written.push({ client, file });\n }\n return written;\n}\n\n// ---------------------------------------------------------------------------\n// Single-app connect\n// ---------------------------------------------------------------------------\n\nasync function connectOne(\n rawUrl: string,\n parsed: ParsedConnectArgs,\n deps: ConnectDeps,\n): Promise<{ ok: boolean; serverName?: string; files?: string[] }> {\n const baseUrl = normalizeUrl(rawUrl);\n const appSlug = appSlugFromUrl(baseUrl);\n const clients = resolveClients(parsed.client);\n const scope = parsed.scope === \"user\" ? \"user\" : \"project\";\n\n let token: string;\n let mcpUrl: string;\n let serverName: string;\n\n if (parsed.token) {\n // No-browser fallback: skip the device flow entirely.\n token = parsed.token;\n mcpUrl = `${baseUrl}/_agent-native/mcp`;\n serverName = parsed.name ?? defaultServerName(baseUrl);\n logOut(\"\");\n logOut(` Using supplied --token for ${baseUrl} (skipping browser flow).`);\n } else {\n const grant = await runDeviceFlow(baseUrl, appSlug, parsed.client, deps);\n if (!grant) return { ok: false };\n token = grant.token;\n mcpUrl = grant.mcpUrl;\n serverName = parsed.name ?? grant.serverName ?? defaultServerName(baseUrl);\n }\n\n const written = writeConfigs(clients, serverName, mcpUrl, token, scope);\n\n logOut(\"\");\n logOut(` Connected \"${serverName}\" → ${mcpUrl}`);\n for (const w of written) {\n logOut(` ${w.client.padEnd(18)} ${w.file}`);\n }\n logOut(\"\");\n logOut(\" Restart your coding agent to pick up the new MCP server.\");\n return { ok: true, serverName, files: written.map((w) => w.file) };\n}\n\n// ---------------------------------------------------------------------------\n// --all : connect every first-party hosted app\n// ---------------------------------------------------------------------------\n\n/** Hosted first-party apps: visible (non-hidden) templates with a prodUrl. */\nexport function hostedApps(): { name: string; url: string }[] {\n return visibleTemplates()\n .filter((t) => typeof t.prodUrl === \"string\" && t.prodUrl.length > 0)\n .map((t) => ({ name: t.name, url: t.prodUrl as string }));\n}\n\nasync function connectAll(\n parsed: ParsedConnectArgs,\n deps: ConnectDeps,\n): Promise<boolean> {\n const apps = hostedApps();\n if (apps.length === 0) {\n logErr(\" No hosted first-party apps found in the template registry.\");\n return false;\n }\n logOut(\"\");\n logOut(` Connecting ${apps.length} first-party hosted apps…`);\n\n const results: { name: string; status: string; files: string[] }[] = [];\n for (const app of apps) {\n logOut(\"\");\n logOut(` ── ${app.name} (${app.url}) ──`);\n try {\n const res = await connectOne(app.url, parsed, deps);\n results.push({\n name: app.name,\n status: res.ok ? \"connected\" : \"skipped\",\n files: res.files ?? [],\n });\n } catch (err: any) {\n logErr(` ${app.name}: ${err?.message ?? err}`);\n results.push({ name: app.name, status: \"error\", files: [] });\n }\n }\n\n logOut(\"\");\n logOut(\" Summary\");\n for (const r of results) {\n const files = r.files.length ? r.files.join(\", \") : \"—\";\n logOut(` ${r.name.padEnd(14)} ${r.status.padEnd(10)} ${files}`);\n }\n return results.every((r) => r.status === \"connected\");\n}\n\n// ---------------------------------------------------------------------------\n// Entry point\n// ---------------------------------------------------------------------------\n\nconst HELP = `agent-native connect — wire your coding agent to a deployed app\n\nUsage:\n agent-native connect <url> [--client <c>] [--scope user|project] [--name <n>]\n Browser device-code flow. Prints a code, opens the verification URL,\n polls until approved, then writes the HTTP MCP entry into your\n client config(s). Idempotent — re-running replaces the same entry.\n\n agent-native connect <url> --token <token>\n No-browser fallback. Skip the device flow and write the entry with\n the supplied token (get it from the app's Connect page).\n\n agent-native connect --all [--client <c>] [--scope user|project]\n Connect every first-party hosted app at once.\n\nClients: all (default), claude-code, claude-code-cli, codex, cowork\nScope: project (default, .mcp.json) or user (~/.claude.json)`;\n\n/**\n * `agent-native connect` entry point. `deps` is injectable for tests; the\n * dispatcher in index.ts calls it with just `args`.\n *\n * Sets `process.exitCode = 1` on failure (so the process exits non-zero\n * once the event loop drains) rather than calling `process.exit`, keeping\n * the function testable — same pattern as `audit-agent-web`.\n */\nexport async function runConnect(\n args: string[],\n deps: ConnectDeps = {},\n): Promise<void> {\n if (args[0] === \"--help\" || args[0] === \"-h\" || args[0] === \"help\") {\n logOut(HELP);\n return;\n }\n\n const parsed = parseConnectArgs(args);\n\n try {\n if (parsed.all) {\n const ok = await connectAll(parsed, deps);\n if (!ok) process.exitCode = 1;\n return;\n }\n\n if (!parsed.url) {\n logErr(\" Missing app URL.\");\n logErr(\"\");\n logOut(HELP);\n process.exitCode = 1;\n return;\n }\n\n const res = await connectOne(parsed.url, parsed, deps);\n if (!res.ok) process.exitCode = 1;\n } catch (err: any) {\n logErr(` ${err?.message ?? err}`);\n process.exitCode = 1;\n }\n}\n"]}
|
package/dist/cli/index.js
CHANGED
|
@@ -529,6 +529,18 @@ switch (command) {
|
|
|
529
529
|
});
|
|
530
530
|
break;
|
|
531
531
|
}
|
|
532
|
+
case "connect": {
|
|
533
|
+
// Wire your local coding agent to a DEPLOYED agent-native app via a
|
|
534
|
+
// browser device-code flow (no token copying). `--all` connects every
|
|
535
|
+
// first-party hosted app; `--token` is the no-browser fallback.
|
|
536
|
+
import("./connect.js")
|
|
537
|
+
.then((m) => m.runConnect(args))
|
|
538
|
+
.catch((err) => {
|
|
539
|
+
console.error(err?.message ?? err);
|
|
540
|
+
process.exit(1);
|
|
541
|
+
});
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
532
544
|
case "create-workspace": {
|
|
533
545
|
// Deprecated alias for `create` (since workspace is now the default).
|
|
534
546
|
const parsed = parseScaffoldArgs(args);
|
|
@@ -609,6 +621,10 @@ Usage:
|
|
|
609
621
|
cmds: serve | install | uninstall | status |
|
|
610
622
|
token (--client claude-code|claude-code-cli|
|
|
611
623
|
codex|cowork)
|
|
624
|
+
agent-native connect <url> Wire your coding agent to a DEPLOYED app via
|
|
625
|
+
a browser device-code flow. --all connects
|
|
626
|
+
every first-party app; --token is the
|
|
627
|
+
no-browser fallback.
|
|
612
628
|
agent-native migrate <source> Create an Agent-Native Code /migrate session, or use
|
|
613
629
|
--emit for a portable own-agent dossier.
|
|
614
630
|
agent-native add-app [name] Add one or more apps to the current workspace
|