appilot-mcp 0.2.1 → 0.3.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/LICENSE +15 -0
- package/README.md +74 -16
- package/dist/appilot-configurator.mcpb +0 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +171 -0
- package/dist/client.d.ts +45 -1
- package/dist/client.js +74 -1
- package/dist/contract/healthContract.js +31 -4
- package/dist/index.bundle.js +820 -142
- package/dist/index.js +7 -0
- package/dist/manifest.d.ts +14 -2
- package/dist/manifest.js +31 -9
- package/dist/public-marketplace/.claude-plugin/marketplace.json +20 -0
- package/dist/public-marketplace/README.md +23 -0
- package/dist/public-marketplace/plugins/app-configurator/.claude-plugin/plugin.json +43 -0
- package/dist/public-marketplace/plugins/app-configurator/README.md +328 -0
- package/dist/public-marketplace/plugins/app-configurator/dist/index.bundle.js +57370 -0
- package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/SKILL.md +267 -0
- package/dist/public-marketplace/plugins/app-configurator/skills/app-configurator/agents/openai.yaml +13 -0
- package/dist/remote/consent.d.ts +10 -2
- package/dist/remote/consent.js +15 -6
- package/dist/remote/consentMessages.d.ts +4 -1
- package/dist/remote/consentMessages.js +9 -6
- package/dist/remote/httpServer.js +2 -2
- package/dist/remote/oauth.js +9 -9
- package/dist/scaffold.d.ts +68 -6
- package/dist/scaffold.js +424 -97
- package/dist/server.js +175 -18
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/examples/app.appilot.json +212 -0
- package/mcpb/manifest.json +117 -21
- package/package.json +5 -3
- package/skills/app-configurator/SKILL.md +61 -19
package/dist/index.bundle.js
CHANGED
|
@@ -21821,7 +21821,7 @@ var SERVER_VERSION;
|
|
|
21821
21821
|
var init_version = __esm({
|
|
21822
21822
|
"src/version.ts"() {
|
|
21823
21823
|
"use strict";
|
|
21824
|
-
SERVER_VERSION = "0.
|
|
21824
|
+
SERVER_VERSION = "0.3.0";
|
|
21825
21825
|
}
|
|
21826
21826
|
});
|
|
21827
21827
|
|
|
@@ -21859,6 +21859,9 @@ function localized(entity, field) {
|
|
|
21859
21859
|
if (Array.isArray(rows) && rows.length) return fromTranslationRows(rows, field);
|
|
21860
21860
|
return fromBaseAndI18n(entity[field], entity[`${field}_i18n`], entity.source_locale);
|
|
21861
21861
|
}
|
|
21862
|
+
function credentialAdvice(hasToken) {
|
|
21863
|
+
return hasToken ? "The credential was rejected. Check it in the Backoffice under Service tokens: it may be revoked, expired, or scoped to another app. A token narrowed to one app is refused on every other app in the organization." : 'No credential is set. Put a service token in APPILOT_PAT in the MCP client environment and restart the client. Mint one in the Backoffice under Service tokens: "Inspect only" to read and audit, "Edit configuration" to write, "Set up integrations" to create apps, domains and widget keys.';
|
|
21864
|
+
}
|
|
21862
21865
|
function safeJson(text) {
|
|
21863
21866
|
try {
|
|
21864
21867
|
return JSON.parse(text);
|
|
@@ -22007,8 +22010,9 @@ var init_client = __esm({
|
|
|
22007
22010
|
const text = await res.text();
|
|
22008
22011
|
const body = text ? safeJson(text) : void 0;
|
|
22009
22012
|
if (!res.ok) {
|
|
22013
|
+
const credential = res.status === 401 || res.status === 403 ? ` ${credentialAdvice(!!this.conn.token)}` : "";
|
|
22010
22014
|
throw new AppilotApiError(
|
|
22011
|
-
`${init.method ?? "GET"} ${path} failed: ${describeError(body, res)}`,
|
|
22015
|
+
`${init.method ?? "GET"} ${path} failed: ${describeError(body, res)}${credential}`,
|
|
22012
22016
|
res.status,
|
|
22013
22017
|
body
|
|
22014
22018
|
);
|
|
@@ -22112,6 +22116,16 @@ var init_client = __esm({
|
|
|
22112
22116
|
listProvisioned() {
|
|
22113
22117
|
return this.request("/provision/apps");
|
|
22114
22118
|
}
|
|
22119
|
+
/**
|
|
22120
|
+
* The org's widget keys, prefixes only.
|
|
22121
|
+
*
|
|
22122
|
+
* A caller that cannot see the keys it already minted asks for another every
|
|
22123
|
+
* run, and each one is a live credential.
|
|
22124
|
+
*/
|
|
22125
|
+
async listWidgetKeys() {
|
|
22126
|
+
const body = await this.request("/provision/widget-keys");
|
|
22127
|
+
return Array.isArray(body?.widgetKeys) ? body.widgetKeys : [];
|
|
22128
|
+
}
|
|
22115
22129
|
provisionApp(body) {
|
|
22116
22130
|
return this.request("/provision/app", { method: "POST", body: JSON.stringify(body) });
|
|
22117
22131
|
}
|
|
@@ -22128,6 +22142,21 @@ var init_client = __esm({
|
|
|
22128
22142
|
const url2 = /^https?:\/\//i.test(domain) ? domain : `https://${domain}`;
|
|
22129
22143
|
return this.request(`/domain/check?url=${encodeURIComponent(url2)}`);
|
|
22130
22144
|
}
|
|
22145
|
+
/**
|
|
22146
|
+
* The TXT record a domain needs, and where its verification stands.
|
|
22147
|
+
*
|
|
22148
|
+
* These two live on the apps router rather than under `/provision`, and that
|
|
22149
|
+
* router authenticates an organization SESSION. A service token is refused
|
|
22150
|
+
* there today, which is why `verify_domain` falls back to the provisioning
|
|
22151
|
+
* dry run for the status and says plainly what it could not do.
|
|
22152
|
+
*/
|
|
22153
|
+
domainVerification(domainId) {
|
|
22154
|
+
return this.request(`/apps/domains/${domainId}/verification`);
|
|
22155
|
+
}
|
|
22156
|
+
/** Ask Appilot to look for the TXT record now. */
|
|
22157
|
+
triggerDomainVerification(domainId) {
|
|
22158
|
+
return this.request(`/apps/domains/${domainId}/verify`, { method: "POST" });
|
|
22159
|
+
}
|
|
22131
22160
|
/** Who this credential is: org, app narrowing, scopes. Never a secret. */
|
|
22132
22161
|
whoami() {
|
|
22133
22162
|
return this.request("/config/whoami");
|
|
@@ -22180,6 +22209,10 @@ var init_client = __esm({
|
|
|
22180
22209
|
read("knowledge", () => this.listKnowledge(appId), mapKnowledge),
|
|
22181
22210
|
expectedLocales?.length ? Promise.resolve(expectedLocales) : this.resolveLocales(appId)
|
|
22182
22211
|
]);
|
|
22212
|
+
if (knowledge.length === 0 && !gaps.some((g) => g.entity === "knowledge")) {
|
|
22213
|
+
const refusal = await this.credentialRefusal();
|
|
22214
|
+
if (refusal) gaps.push({ entity: "knowledge", reason: refusal });
|
|
22215
|
+
}
|
|
22183
22216
|
return {
|
|
22184
22217
|
expectedLocales: locales,
|
|
22185
22218
|
views,
|
|
@@ -22192,6 +22225,28 @@ var init_client = __esm({
|
|
|
22192
22225
|
...gaps.length ? { gaps } : {}
|
|
22193
22226
|
};
|
|
22194
22227
|
}
|
|
22228
|
+
/**
|
|
22229
|
+
* Why an empty read should not be believed, or null when the credential is
|
|
22230
|
+
* fine and the app genuinely has nothing.
|
|
22231
|
+
*
|
|
22232
|
+
* Only a 401 or a 403 counts. An instance too old to answer `whoami` at all
|
|
22233
|
+
* says nothing about the token, and reporting a gap on that would be a
|
|
22234
|
+
* confident false alarm on every on-premise deployment behind cloud.
|
|
22235
|
+
*/
|
|
22236
|
+
async credentialRefusal() {
|
|
22237
|
+
if (!this.conn.token) {
|
|
22238
|
+
return `No service token is set, so the knowledge read was anonymous and answered an empty list rather than refusing. ${credentialAdvice(false)}`;
|
|
22239
|
+
}
|
|
22240
|
+
try {
|
|
22241
|
+
await this.whoami();
|
|
22242
|
+
return null;
|
|
22243
|
+
} catch (err) {
|
|
22244
|
+
if (err instanceof AppilotApiError && (err.status === 401 || err.status === 403)) {
|
|
22245
|
+
return `The knowledge read returned nothing and the credential failed its own self-check, so the empty result is not evidence of an empty knowledge base. ${credentialAdvice(true)}`;
|
|
22246
|
+
}
|
|
22247
|
+
return null;
|
|
22248
|
+
}
|
|
22249
|
+
}
|
|
22195
22250
|
/**
|
|
22196
22251
|
* Which locales this app's configuration is expected to cover: the union of
|
|
22197
22252
|
* its domains' configured languages. Falls back to the trilingual baseline
|
|
@@ -23024,6 +23079,10 @@ function planMarkers(plan) {
|
|
|
23024
23079
|
function localizedValues(text) {
|
|
23025
23080
|
return Object.values(text).filter((v) => typeof v === "string");
|
|
23026
23081
|
}
|
|
23082
|
+
function authoredFormValues(plan) {
|
|
23083
|
+
const raw = plan.form_values;
|
|
23084
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
23085
|
+
}
|
|
23027
23086
|
function lintMarkers(snap) {
|
|
23028
23087
|
const controlIds = new Set(snap.controls.map((c) => c.semantic_id));
|
|
23029
23088
|
const formIds = new Set(snap.forms.map((f) => f.semantic_id));
|
|
@@ -23041,7 +23100,7 @@ function lintMarkers(snap) {
|
|
|
23041
23100
|
recommendation: "Each step needs exactly one executable marker resolving to a known control/form; values never go in the marker."
|
|
23042
23101
|
});
|
|
23043
23102
|
}
|
|
23044
|
-
for (const [formId, entry] of Object.entries(plan
|
|
23103
|
+
for (const [formId, entry] of Object.entries(authoredFormValues(plan))) {
|
|
23045
23104
|
if (!formIds.has(formId)) {
|
|
23046
23105
|
findings.push({
|
|
23047
23106
|
severity: "high",
|
|
@@ -23051,8 +23110,19 @@ function lintMarkers(snap) {
|
|
|
23051
23110
|
});
|
|
23052
23111
|
continue;
|
|
23053
23112
|
}
|
|
23113
|
+
const authored = entry;
|
|
23114
|
+
if (!authored || !Array.isArray(authored.fields)) {
|
|
23115
|
+
findings.push({
|
|
23116
|
+
severity: "high",
|
|
23117
|
+
category: "markers",
|
|
23118
|
+
entity: `action_plan:${plan.semantic_id}`,
|
|
23119
|
+
message: `form_values["${formId}"] is not in the authored shape { fields: [{ control_id, value }] }.`,
|
|
23120
|
+
recommendation: "Write an empty fields array when there are no authored defaults."
|
|
23121
|
+
});
|
|
23122
|
+
continue;
|
|
23123
|
+
}
|
|
23054
23124
|
const fields = fieldsByForm.get(formId) ?? /* @__PURE__ */ new Set();
|
|
23055
|
-
for (const f of
|
|
23125
|
+
for (const f of authored.fields) {
|
|
23056
23126
|
if (!fields.has(f.control_id)) {
|
|
23057
23127
|
findings.push({
|
|
23058
23128
|
severity: "high",
|
|
@@ -23075,7 +23145,7 @@ function lintActionability(snap) {
|
|
|
23075
23145
|
const markers = planMarkers(plan);
|
|
23076
23146
|
const hasFormStep = markers.some((m) => m.kind === "form");
|
|
23077
23147
|
const hasSetValue = markers.some((m) => m.kind === "set_value" || m.kind === "select");
|
|
23078
|
-
const hasFormValues = Object.keys(plan
|
|
23148
|
+
const hasFormValues = Object.keys(authoredFormValues(plan)).length > 0;
|
|
23079
23149
|
const entersValue = hasFormStep || hasSetValue || hasFormValues;
|
|
23080
23150
|
const looksLikeCreate = localizedValues({ ...plan.name, ...plan.description }).some((v) => CREATE_INTENT.test(v)) || CREATE_INTENT.test(plan.semantic_id);
|
|
23081
23151
|
const formFieldIds = new Set(snap.forms.flatMap((f) => f.field_ids));
|
|
@@ -23093,7 +23163,7 @@ function lintActionability(snap) {
|
|
|
23093
23163
|
if (!form.submit_control_id) continue;
|
|
23094
23164
|
const clicksSubmit = markers.some((m) => m.kind === "click" && m.id === form.submit_control_id);
|
|
23095
23165
|
if (!clicksSubmit) continue;
|
|
23096
|
-
const fillsForm = markers.some((m) => m.kind === "form" && m.id === form.semantic_id) || (plan
|
|
23166
|
+
const fillsForm = markers.some((m) => m.kind === "form" && m.id === form.semantic_id) || (authoredFormValues(plan)[form.semantic_id]?.fields?.length ?? 0) > 0;
|
|
23097
23167
|
if (!fillsForm) {
|
|
23098
23168
|
findings.push({
|
|
23099
23169
|
severity: "high",
|
|
@@ -23441,6 +23511,17 @@ async function planManifest(client, manifest, resolveAppId) {
|
|
|
23441
23511
|
}
|
|
23442
23512
|
return { planToken: manifestDigest(manifest), provisioning, config: config2, notes };
|
|
23443
23513
|
}
|
|
23514
|
+
function provisioningChanges(preview) {
|
|
23515
|
+
const changes = [];
|
|
23516
|
+
if (preview.app.action !== "reused") changes.push(`the app "${preview.app.name}" (${preview.app.action})`);
|
|
23517
|
+
for (const domain of preview.domains) {
|
|
23518
|
+
if (domain.action !== "reused") changes.push(`the domain ${domain.domain} (${domain.action})`);
|
|
23519
|
+
}
|
|
23520
|
+
if (preview.widgetKey && preview.widgetKey.action !== "reused") {
|
|
23521
|
+
changes.push(`a widget key (${preview.widgetKey.action})`);
|
|
23522
|
+
}
|
|
23523
|
+
return changes;
|
|
23524
|
+
}
|
|
23444
23525
|
async function applyManifest(client, manifest, options) {
|
|
23445
23526
|
const expected = manifestDigest(manifest);
|
|
23446
23527
|
if (options.planToken !== expected) {
|
|
@@ -23449,7 +23530,23 @@ async function applyManifest(client, manifest, options) {
|
|
|
23449
23530
|
);
|
|
23450
23531
|
}
|
|
23451
23532
|
const notes = [];
|
|
23452
|
-
const
|
|
23533
|
+
const preview = await client.provisionApp(provisionRequest(manifest, true));
|
|
23534
|
+
const changes = provisioningChanges(preview);
|
|
23535
|
+
if (changes.length > 0 && options.provisionRefusal) {
|
|
23536
|
+
throw new Error(
|
|
23537
|
+
`${options.provisionRefusal}
|
|
23538
|
+
This manifest would change ${changes.join(", ")}, so the apply cannot proceed without it. A manifest whose app, domains and keys already exist applies with config:write alone.`
|
|
23539
|
+
);
|
|
23540
|
+
}
|
|
23541
|
+
let provisioning;
|
|
23542
|
+
if (changes.length === 0) {
|
|
23543
|
+
provisioning = preview;
|
|
23544
|
+
notes.push(
|
|
23545
|
+
"Provisioning wrote nothing: the app, its domains and its keys already matched the manifest, so this apply needed only config:write. The provisioning block below is the preview, which is why it reports dryRun true."
|
|
23546
|
+
);
|
|
23547
|
+
} else {
|
|
23548
|
+
provisioning = await client.provisionApp(provisionRequest(manifest, false));
|
|
23549
|
+
}
|
|
23453
23550
|
let config2;
|
|
23454
23551
|
if (manifest.config) {
|
|
23455
23552
|
const appId = provisioning.app.id;
|
|
@@ -23487,6 +23584,32 @@ var init_manifest = __esm({
|
|
|
23487
23584
|
});
|
|
23488
23585
|
|
|
23489
23586
|
// src/scaffold.ts
|
|
23587
|
+
function publicKeyIdiom(framework) {
|
|
23588
|
+
switch (framework) {
|
|
23589
|
+
case "next":
|
|
23590
|
+
return {
|
|
23591
|
+
envName: "NEXT_PUBLIC_APPILOT_WIDGET_KEY",
|
|
23592
|
+
expr: "process.env.NEXT_PUBLIC_APPILOT_WIDGET_KEY"
|
|
23593
|
+
};
|
|
23594
|
+
case "sveltekit":
|
|
23595
|
+
return {
|
|
23596
|
+
envName: "PUBLIC_APPILOT_WIDGET_KEY",
|
|
23597
|
+
expr: "publicEnv.PUBLIC_APPILOT_WIDGET_KEY",
|
|
23598
|
+
importLine: "import { env as publicEnv } from '$env/dynamic/public';"
|
|
23599
|
+
};
|
|
23600
|
+
default:
|
|
23601
|
+
return {
|
|
23602
|
+
envName: "VITE_APPILOT_WIDGET_KEY",
|
|
23603
|
+
expr: "import.meta.env.VITE_APPILOT_WIDGET_KEY"
|
|
23604
|
+
};
|
|
23605
|
+
}
|
|
23606
|
+
}
|
|
23607
|
+
function localDevelopmentNotes(idiom) {
|
|
23608
|
+
return [
|
|
23609
|
+
"Local development, first choice: run the app on a hostname that resolves to 127.0.0.1 (myapp.lvh.me:3000, or an /etc/hosts entry such as myapp.local) and register THAT hostname as a domain of the app. The page then resolves its tenant by its own hostname, no widget key is needed at all, DNS verification is not required for this path, and the turn gets the full app context: views, controls, forms, action plans and knowledge.",
|
|
23610
|
+
`Local development, fallback: on bare localhost no domain resolves, so the page needs a wk_test_ key in ${idiom.envName}. The turn gets the organization and the signed-in user and NO app context, because there is no domain to resolve it from. Use this only where the hostname above is impossible.`
|
|
23611
|
+
];
|
|
23612
|
+
}
|
|
23490
23613
|
function relayFetchHandler(namespace) {
|
|
23491
23614
|
return `import { createWidgetTokenHandler } from 'appilot-server';
|
|
23492
23615
|
|
|
@@ -23508,36 +23631,70 @@ export const POST = createWidgetTokenHandler({
|
|
|
23508
23631
|
`;
|
|
23509
23632
|
}
|
|
23510
23633
|
function relayNodeHandler(namespace, framework) {
|
|
23511
|
-
const
|
|
23512
|
-
|
|
23634
|
+
const head = `import type { IncomingMessage } from 'node:http';
|
|
23635
|
+
import { createNodeWidgetTokenHandler } from 'appilot-server/node';
|
|
23636
|
+
|
|
23637
|
+
/** What your session middleware puts on the request. Yours will differ. */
|
|
23638
|
+
type RequestWithSession = IncomingMessage & {
|
|
23639
|
+
session?: { user?: { id: string | number; name?: string } };
|
|
23640
|
+
};
|
|
23513
23641
|
|
|
23514
23642
|
// resolveUser is the security boundary. Derive the user from a credential you
|
|
23515
23643
|
// trust (req.session, req.user). Never from the request body.
|
|
23516
23644
|
const relay = createNodeWidgetTokenHandler({
|
|
23517
|
-
apiUrl: process.env.APPILOT_API_URL
|
|
23518
|
-
widgetKey: process.env.APPILOT_WIDGET_KEY
|
|
23519
|
-
widgetSecret: process.env.APPILOT_WIDGET_SECRET
|
|
23645
|
+
apiUrl: process.env.APPILOT_API_URL!,
|
|
23646
|
+
widgetKey: process.env.APPILOT_WIDGET_KEY!,
|
|
23647
|
+
widgetSecret: process.env.APPILOT_WIDGET_SECRET!, // server-side only
|
|
23520
23648
|
resolveUser: req => {
|
|
23521
|
-
const user = req.session?.user;
|
|
23522
|
-
if (!user) return null;
|
|
23649
|
+
const user = (req as RequestWithSession).session?.user; // your auth, unchanged
|
|
23650
|
+
if (!user) return null; // 401 IDENTITY_REQUIRED
|
|
23523
23651
|
return { externalId: \`${namespace}:\${user.id}\`, displayName: user.name };
|
|
23524
23652
|
},
|
|
23525
23653
|
});
|
|
23654
|
+
`;
|
|
23655
|
+
if (framework === "express") {
|
|
23656
|
+
return `${head}
|
|
23657
|
+
import type { Express } from 'express';
|
|
23658
|
+
|
|
23659
|
+
export function mountWidgetToken(app: Express): void {
|
|
23660
|
+
app.post('/api/widget/token', relay);
|
|
23661
|
+
}
|
|
23662
|
+
`;
|
|
23663
|
+
}
|
|
23664
|
+
return `${head}
|
|
23665
|
+
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
|
23526
23666
|
|
|
23527
|
-
|
|
23667
|
+
export function mountWidgetToken(fastify: FastifyInstance): void {
|
|
23668
|
+
fastify.post('/api/widget/token', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
23669
|
+
// The relay answers the raw response itself, so Fastify has to be told it
|
|
23670
|
+
// no longer owns the reply. Without hijack() it reports "Reply was already
|
|
23671
|
+
// sent" on every call.
|
|
23672
|
+
reply.hijack();
|
|
23673
|
+
await relay(request.raw, reply.raw);
|
|
23674
|
+
});
|
|
23675
|
+
}
|
|
23528
23676
|
`;
|
|
23529
23677
|
}
|
|
23530
23678
|
function bootFile(options) {
|
|
23531
|
-
const
|
|
23532
|
-
|
|
23679
|
+
const idiom = publicKeyIdiom(options.framework);
|
|
23680
|
+
const lines = ["import { bootAppilotWidget } from 'appilot';"];
|
|
23681
|
+
if (idiom.importLine) lines.push(idiom.importLine);
|
|
23682
|
+
lines.push(
|
|
23683
|
+
"",
|
|
23684
|
+
"// The widget key is publishable: it names the app to Appilot and is meant to",
|
|
23685
|
+
"// sit in the page. The widget SECRET is a server credential and never appears",
|
|
23686
|
+
`// in this file. Leave ${idiom.envName} unset when the page runs on a hostname`,
|
|
23687
|
+
"// you registered as a domain: the tenant then resolves from the hostname and",
|
|
23688
|
+
"// the turn gets the full app context.",
|
|
23689
|
+
`const widgetKey = ${idiom.expr};`,
|
|
23533
23690
|
"",
|
|
23534
23691
|
"// Call once, after your app knows the user is signed in. The widget",
|
|
23535
|
-
"// requires an identified user; the relay
|
|
23692
|
+
"// requires an identified user; the relay mints that identity.",
|
|
23536
23693
|
"export function startAppilot() {",
|
|
23537
23694
|
" bootAppilotWidget({",
|
|
23538
|
-
` widgetScriptUrl: '${options.widgetScriptUrl}'
|
|
23539
|
-
|
|
23540
|
-
|
|
23695
|
+
` widgetScriptUrl: '${options.widgetScriptUrl}',`,
|
|
23696
|
+
" widgetKey,"
|
|
23697
|
+
);
|
|
23541
23698
|
if (options.apiUrl) lines.push(` appilotApiUrl: '${options.apiUrl}',`);
|
|
23542
23699
|
lines.push(
|
|
23543
23700
|
" tokenEndpoint: '/api/widget/token',",
|
|
@@ -23546,8 +23703,142 @@ function bootFile(options) {
|
|
|
23546
23703
|
);
|
|
23547
23704
|
return lines.join("\n") + "\n";
|
|
23548
23705
|
}
|
|
23706
|
+
function rawExchangeFiles(namespace, apiUrl) {
|
|
23707
|
+
return [
|
|
23708
|
+
{
|
|
23709
|
+
path: "appilot/widget-token.sh",
|
|
23710
|
+
language: "bash",
|
|
23711
|
+
contents: `# The whole relay, as one call. Run it from your BACKEND, never a browser:
|
|
23712
|
+
# the widget secret authenticates the exchange and must never leave the server.
|
|
23713
|
+
curl -sS -X POST '${apiUrl}/widget/token' \\
|
|
23714
|
+
-H 'Content-Type: application/json' \\
|
|
23715
|
+
-H "X-Widget-Key: $APPILOT_WIDGET_KEY" \\
|
|
23716
|
+
-H "X-Widget-Secret: $APPILOT_WIDGET_SECRET" \\
|
|
23717
|
+
-d '{"externalId":"${namespace}:4711","displayName":"Ada Lovelace"}'
|
|
23718
|
+
|
|
23719
|
+
# 200 -> {"token":"...","expiresIn":900}. Return that body to the page as-is.
|
|
23720
|
+
`
|
|
23721
|
+
},
|
|
23722
|
+
{
|
|
23723
|
+
path: "appilot/widget_token.py",
|
|
23724
|
+
language: "python",
|
|
23725
|
+
contents: `"""Appilot identity relay, Django view. Flask and FastAPI differ only in the
|
|
23726
|
+
decorator and the response helper.
|
|
23727
|
+
|
|
23728
|
+
The user is resolved from the session the host already trusts. Never from the
|
|
23729
|
+
request body: the body is client-supplied, so reading an id from it would let
|
|
23730
|
+
any caller ask for any user's token.
|
|
23731
|
+
"""
|
|
23732
|
+
|
|
23733
|
+
import os
|
|
23734
|
+
import json
|
|
23735
|
+
import urllib.request
|
|
23736
|
+
|
|
23737
|
+
APPILOT_API_URL = os.environ["APPILOT_API_URL"]
|
|
23738
|
+
WIDGET_KEY = os.environ["APPILOT_WIDGET_KEY"]
|
|
23739
|
+
WIDGET_SECRET = os.environ["APPILOT_WIDGET_SECRET"] # server-side only
|
|
23740
|
+
NAMESPACE = "${namespace}"
|
|
23741
|
+
|
|
23742
|
+
|
|
23743
|
+
def widget_token(request):
|
|
23744
|
+
from django.http import JsonResponse
|
|
23745
|
+
|
|
23746
|
+
user = getattr(request, "user", None)
|
|
23747
|
+
if user is None or not user.is_authenticated:
|
|
23748
|
+
return JsonResponse({"code": "IDENTITY_REQUIRED"}, status=401)
|
|
23749
|
+
|
|
23750
|
+
payload = json.dumps(
|
|
23751
|
+
{
|
|
23752
|
+
"externalId": f"{NAMESPACE}:{user.pk}",
|
|
23753
|
+
"displayName": user.get_full_name() or user.get_username(),
|
|
23754
|
+
}
|
|
23755
|
+
).encode()
|
|
23756
|
+
|
|
23757
|
+
req = urllib.request.Request(
|
|
23758
|
+
f"{APPILOT_API_URL}/widget/token",
|
|
23759
|
+
data=payload,
|
|
23760
|
+
headers={
|
|
23761
|
+
"Content-Type": "application/json",
|
|
23762
|
+
"X-Widget-Key": WIDGET_KEY,
|
|
23763
|
+
"X-Widget-Secret": WIDGET_SECRET,
|
|
23764
|
+
},
|
|
23765
|
+
method="POST",
|
|
23766
|
+
)
|
|
23767
|
+
try:
|
|
23768
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
23769
|
+
body = json.load(response)
|
|
23770
|
+
except Exception:
|
|
23771
|
+
# Never forward the upstream message: it is not yours to surface.
|
|
23772
|
+
return JsonResponse({"code": "WIDGET_TOKEN_EXCHANGE_FAILED"}, status=502)
|
|
23773
|
+
|
|
23774
|
+
return JsonResponse({"token": body["token"], "expiresIn": body["expiresIn"]})
|
|
23775
|
+
`
|
|
23776
|
+
},
|
|
23777
|
+
{
|
|
23778
|
+
path: "appilot/widget_token_controller.rb",
|
|
23779
|
+
language: "ruby",
|
|
23780
|
+
contents: `# Appilot identity relay, Rails controller.
|
|
23781
|
+
#
|
|
23782
|
+
# current_user comes from the session Rails already established. Never from
|
|
23783
|
+
# params: those are client-supplied, and reading an id from them would let any
|
|
23784
|
+
# caller ask for any user's token.
|
|
23785
|
+
require "net/http"
|
|
23786
|
+
require "json"
|
|
23787
|
+
|
|
23788
|
+
class WidgetTokenController < ApplicationController
|
|
23789
|
+
NAMESPACE = "${namespace}".freeze
|
|
23790
|
+
|
|
23791
|
+
def create
|
|
23792
|
+
return render(json: { code: "IDENTITY_REQUIRED" }, status: :unauthorized) unless current_user
|
|
23793
|
+
|
|
23794
|
+
uri = URI("#{ENV.fetch('APPILOT_API_URL')}/widget/token")
|
|
23795
|
+
request = Net::HTTP::Post.new(uri)
|
|
23796
|
+
request["Content-Type"] = "application/json"
|
|
23797
|
+
request["X-Widget-Key"] = ENV.fetch("APPILOT_WIDGET_KEY")
|
|
23798
|
+
request["X-Widget-Secret"] = ENV.fetch("APPILOT_WIDGET_SECRET") # server-side only
|
|
23799
|
+
request.body = {
|
|
23800
|
+
externalId: "#{NAMESPACE}:#{current_user.id}",
|
|
23801
|
+
displayName: current_user.name
|
|
23802
|
+
}.to_json
|
|
23803
|
+
|
|
23804
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 10) do |http|
|
|
23805
|
+
http.request(request)
|
|
23806
|
+
end
|
|
23807
|
+
|
|
23808
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
23809
|
+
# Never forward the upstream message.
|
|
23810
|
+
return render(json: { code: "WIDGET_TOKEN_EXCHANGE_FAILED" }, status: :bad_gateway)
|
|
23811
|
+
end
|
|
23812
|
+
|
|
23813
|
+
body = JSON.parse(response.body)
|
|
23814
|
+
render json: { token: body["token"], expiresIn: body["expiresIn"] }
|
|
23815
|
+
end
|
|
23816
|
+
end
|
|
23817
|
+
`
|
|
23818
|
+
}
|
|
23819
|
+
];
|
|
23820
|
+
}
|
|
23821
|
+
function rawBootFile(options) {
|
|
23822
|
+
const keyAttr = options.widgetKey ? `
|
|
23823
|
+
data-api-key="${options.widgetKey}"` : "";
|
|
23824
|
+
const apiAttr = options.apiUrl ? `
|
|
23825
|
+
data-api-url="${options.apiUrl}"` : "";
|
|
23826
|
+
return {
|
|
23827
|
+
path: "appilot/widget-snippet.html",
|
|
23828
|
+
language: "html",
|
|
23829
|
+
contents: `<!-- Paste into the layout template your signed-in pages render.
|
|
23830
|
+
The relay above must answer at /api/widget/token on the same origin. -->
|
|
23831
|
+
<script
|
|
23832
|
+
src="${options.widgetScriptUrl}"${keyAttr}${apiAttr}
|
|
23833
|
+
async
|
|
23834
|
+
></script>
|
|
23835
|
+
`
|
|
23836
|
+
};
|
|
23837
|
+
}
|
|
23549
23838
|
function scaffoldIntegration(options) {
|
|
23550
23839
|
const namespace = options.idNamespace ?? "app";
|
|
23840
|
+
const apiUrl = options.apiUrl ?? "https://api.appilot.space";
|
|
23841
|
+
const idiom = publicKeyIdiom(options.framework);
|
|
23551
23842
|
const files = [];
|
|
23552
23843
|
switch (options.framework) {
|
|
23553
23844
|
case "next":
|
|
@@ -23584,18 +23875,21 @@ export const action = ({ request }: { request: Request }) => handler(request);
|
|
|
23584
23875
|
language: "typescript",
|
|
23585
23876
|
contents: `import { createWidgetTokenHandler } from 'appilot-server';
|
|
23586
23877
|
import { env } from '$env/dynamic/private';
|
|
23878
|
+
import type { RequestHandler } from './$types';
|
|
23587
23879
|
|
|
23588
23880
|
const handler = createWidgetTokenHandler({
|
|
23589
|
-
|
|
23590
|
-
|
|
23591
|
-
|
|
23881
|
+
// $env/dynamic/private is Record<string, string | undefined>, so each value is
|
|
23882
|
+
// asserted here rather than handed to the relay as possibly-undefined.
|
|
23883
|
+
apiUrl: env.APPILOT_API_URL!,
|
|
23884
|
+
widgetKey: env.APPILOT_WIDGET_KEY!,
|
|
23885
|
+
widgetSecret: env.APPILOT_WIDGET_SECRET!,
|
|
23592
23886
|
resolveUser: async request => {
|
|
23593
23887
|
const user = await getUserFromCookies(request.headers.get('cookie'));
|
|
23594
23888
|
return user ? { externalId: \`${namespace}:\${user.id}\`, displayName: user.name } : null;
|
|
23595
23889
|
},
|
|
23596
23890
|
});
|
|
23597
23891
|
|
|
23598
|
-
export const POST = ({ request }) => handler(request);
|
|
23892
|
+
export const POST: RequestHandler = ({ request }) => handler(request);
|
|
23599
23893
|
`
|
|
23600
23894
|
});
|
|
23601
23895
|
break;
|
|
@@ -23628,38 +23922,95 @@ export const widgetToken = new Hono().post('/api/widget/token', c => handler(c.r
|
|
|
23628
23922
|
contents: relayNodeHandler(namespace, options.framework)
|
|
23629
23923
|
});
|
|
23630
23924
|
break;
|
|
23925
|
+
case "other":
|
|
23926
|
+
files.push(...rawExchangeFiles(namespace, apiUrl));
|
|
23927
|
+
break;
|
|
23928
|
+
}
|
|
23929
|
+
if (options.framework === "other") {
|
|
23930
|
+
files.push(rawBootFile(options));
|
|
23931
|
+
} else {
|
|
23932
|
+
files.push({ path: "src/appilot/boot.ts", language: "typescript", contents: bootFile(options) });
|
|
23933
|
+
files.push({
|
|
23934
|
+
path: "src/appilot/actions.ts",
|
|
23935
|
+
language: "typescript",
|
|
23936
|
+
contents: CLIENT_ACTION_EXAMPLE
|
|
23937
|
+
});
|
|
23938
|
+
}
|
|
23939
|
+
const serverEnv = [
|
|
23940
|
+
`APPILOT_API_URL=${apiUrl}`,
|
|
23941
|
+
`APPILOT_WIDGET_KEY=${options.widgetKey ?? "wk_test_..."}`,
|
|
23942
|
+
"APPILOT_WIDGET_SECRET=wsk_secret_... # server-side only, never in a client bundle"
|
|
23943
|
+
];
|
|
23944
|
+
if (options.framework !== "other") {
|
|
23945
|
+
serverEnv.push(
|
|
23946
|
+
`${idiom.envName}=${options.widgetKey ?? "wk_test_..."} # the same publishable key, this time for the page`
|
|
23947
|
+
);
|
|
23948
|
+
}
|
|
23949
|
+
const notes = [
|
|
23950
|
+
"The widget secret must never appear in a client bundle or a public env var. In Next.js that means no NEXT_PUBLIC_ prefix; in Vite, keep it out of VITE_.",
|
|
23951
|
+
"resolveUser is the security boundary: derive the user from a credential you trust, never from the request body.",
|
|
23952
|
+
...localDevelopmentNotes(idiom),
|
|
23953
|
+
"Call verify_integration once this is wired to confirm the relay answers and the widget actually boots."
|
|
23954
|
+
];
|
|
23955
|
+
if (options.framework === "other") {
|
|
23956
|
+
notes.unshift(
|
|
23957
|
+
"appilot-server is a Node package, so this host implements the exchange itself. It is one authenticated POST to {apiUrl}/widget/token carrying X-Widget-Key and X-Widget-Secret, and the response body goes back to the page unchanged."
|
|
23958
|
+
);
|
|
23631
23959
|
}
|
|
23632
|
-
files.push({ path: "src/appilot/boot.ts", language: "typescript", contents: bootFile(options) });
|
|
23633
|
-
files.push({
|
|
23634
|
-
path: "src/appilot/actions.ts",
|
|
23635
|
-
language: "typescript",
|
|
23636
|
-
contents: CLIENT_ACTION_EXAMPLE
|
|
23637
|
-
});
|
|
23638
23960
|
return {
|
|
23639
23961
|
framework: options.framework,
|
|
23640
23962
|
files,
|
|
23641
|
-
install: "npm install appilot appilot-server",
|
|
23642
|
-
env:
|
|
23643
|
-
|
|
23644
|
-
`APPILOT_WIDGET_KEY=${options.widgetKey ?? "wk_live_..."}`,
|
|
23645
|
-
"APPILOT_WIDGET_SECRET=wsk_secret_... # server-side only, never in a client bundle"
|
|
23646
|
-
].join("\n"),
|
|
23647
|
-
notes: [
|
|
23648
|
-
"The widget secret must never appear in a client bundle or a public env var. In Next.js that means no NEXT_PUBLIC_ prefix; in Vite, keep it out of VITE_.",
|
|
23649
|
-
"resolveUser is the security boundary: derive the user from a credential you trust, never from the request body.",
|
|
23650
|
-
"Call verify_integration once this is wired to confirm the relay answers and the widget actually boots."
|
|
23651
|
-
]
|
|
23963
|
+
install: options.framework === "other" ? "No package to install. The relay is one HTTPS call and the widget loads from the script tag above." : "npm install appilot appilot-server",
|
|
23964
|
+
env: serverEnv.join("\n"),
|
|
23965
|
+
notes
|
|
23652
23966
|
};
|
|
23653
23967
|
}
|
|
23968
|
+
function toolNameFrom(slug) {
|
|
23969
|
+
return IDENTIFIER_KINDS.tool_name.normalize(slug) || "do_the_thing";
|
|
23970
|
+
}
|
|
23971
|
+
function semanticSlugFrom(slug) {
|
|
23972
|
+
return IDENTIFIER_KINDS.semantic_id.normalize(slug) || "the-thing";
|
|
23973
|
+
}
|
|
23974
|
+
function planSections(shape, capability, ids, viewPath) {
|
|
23975
|
+
if (shape === "read") return null;
|
|
23976
|
+
if (shape === "navigate") {
|
|
23977
|
+
return [
|
|
23978
|
+
{
|
|
23979
|
+
view_path: viewPath,
|
|
23980
|
+
steps: [`Open [${capability}]({{click:${ids.open}}}).`]
|
|
23981
|
+
}
|
|
23982
|
+
];
|
|
23983
|
+
}
|
|
23984
|
+
return [
|
|
23985
|
+
{
|
|
23986
|
+
view_path: viewPath,
|
|
23987
|
+
steps: [
|
|
23988
|
+
`Open the form with [${capability}]({{click:${ids.open}}}).`,
|
|
23989
|
+
`Fill in [the ${capability.toLowerCase()} form]({{form:${ids.form}}}).`,
|
|
23990
|
+
`Submit it with [Save]({{click:${ids.submit}}}).`
|
|
23991
|
+
]
|
|
23992
|
+
}
|
|
23993
|
+
];
|
|
23994
|
+
}
|
|
23654
23995
|
function scaffoldAgentFirst(options) {
|
|
23655
|
-
const { capability,
|
|
23656
|
-
const
|
|
23996
|
+
const { capability, appId } = options;
|
|
23997
|
+
const shape = options.shape ?? "create";
|
|
23998
|
+
const viewPath = options.viewPath ?? "/";
|
|
23999
|
+
const toolName = toolNameFrom(options.slug);
|
|
24000
|
+
const slug = semanticSlugFrom(options.slug);
|
|
24001
|
+
const ids = {
|
|
24002
|
+
open: `btn-${slug}`,
|
|
24003
|
+
form: `form-${slug}`,
|
|
24004
|
+
submit: `submit-${slug}`
|
|
24005
|
+
};
|
|
24006
|
+
const method = options.endpoint?.method?.toUpperCase() ?? (shape === "read" ? "GET" : "POST");
|
|
23657
24007
|
const pathTemplate = options.endpoint?.path ?? `/api/${slug}`;
|
|
23658
|
-
const
|
|
24008
|
+
const trigger = `Use when the user wants to ${capability.toLowerCase()}.`;
|
|
24009
|
+
const tool = options.clientSide ? null : {
|
|
23659
24010
|
app_id: appId,
|
|
23660
|
-
tool_name:
|
|
24011
|
+
tool_name: toolName,
|
|
23661
24012
|
title: capability,
|
|
23662
|
-
description:
|
|
24013
|
+
description: trigger,
|
|
23663
24014
|
parameters: {
|
|
23664
24015
|
type: "object",
|
|
23665
24016
|
properties: {},
|
|
@@ -23674,22 +24025,17 @@ function scaffoldAgentFirst(options) {
|
|
|
23674
24025
|
title_i18n: {},
|
|
23675
24026
|
description_i18n: {}
|
|
23676
24027
|
};
|
|
23677
|
-
const
|
|
24028
|
+
const sections = planSections(shape, capability, ids, viewPath);
|
|
24029
|
+
const actionPlan = sections && {
|
|
23678
24030
|
app_id: appId,
|
|
23679
|
-
semantic_id: `plan-${slug}
|
|
24031
|
+
semantic_id: IDENTIFIER_KINDS.plan_id.normalize(`plan-${slug}`),
|
|
23680
24032
|
name: capability,
|
|
23681
|
-
description:
|
|
23682
|
-
sections
|
|
23683
|
-
|
|
23684
|
-
|
|
23685
|
-
|
|
23686
|
-
|
|
23687
|
-
{ text: `Fill it in: [the ${slug} form]({{form:form-${slug}}})` },
|
|
23688
|
-
{ text: `Submit: [Save]({{click:btn-${slug}-submit}})` }
|
|
23689
|
-
]
|
|
23690
|
-
}
|
|
23691
|
-
],
|
|
23692
|
-
form_values: { [`form-${slug}`]: {} },
|
|
24033
|
+
description: trigger,
|
|
24034
|
+
sections,
|
|
24035
|
+
// Keyed by form id, and each entry carries a `fields` array of
|
|
24036
|
+
// { control_id, value }. The empty array is the authored default: the
|
|
24037
|
+
// user supplies the values at runtime.
|
|
24038
|
+
form_values: shape === "create" ? { [ids.form]: { fields: [] } } : {},
|
|
23693
24039
|
step_narratives_i18n: {},
|
|
23694
24040
|
is_active: false
|
|
23695
24041
|
};
|
|
@@ -23711,6 +24057,7 @@ function scaffoldAgentFirst(options) {
|
|
|
23711
24057
|
};
|
|
23712
24058
|
const files = [];
|
|
23713
24059
|
if (options.clientSide) {
|
|
24060
|
+
const registerName = toolName.replace(/(^|_)([a-z])/g, (_m, _s, c) => c.toUpperCase());
|
|
23714
24061
|
files.push({
|
|
23715
24062
|
path: `src/appilot/${slug}.ts`,
|
|
23716
24063
|
language: "typescript",
|
|
@@ -23720,11 +24067,11 @@ function scaffoldAgentFirst(options) {
|
|
|
23720
24067
|
//
|
|
23721
24068
|
// This runs in the page, in the user's own session, so it inherits their
|
|
23722
24069
|
// permissions and needs no credential of its own. Anything the backend must
|
|
23723
|
-
// authorize belongs in
|
|
23724
|
-
export function register${
|
|
24070
|
+
// authorize belongs in an HTTP-proxy tool instead.
|
|
24071
|
+
export function register${registerName}() {
|
|
23725
24072
|
const handle = registerTool({
|
|
23726
|
-
name: '${
|
|
23727
|
-
description: '
|
|
24073
|
+
name: '${toolName}',
|
|
24074
|
+
description: '${trigger}',
|
|
23728
24075
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
23729
24076
|
// Omit readOnlyHint, or set it false, when this changes something: the
|
|
23730
24077
|
// agent confirms with the user before calling a mutating action.
|
|
@@ -23738,31 +24085,83 @@ export function register${slug.replace(/(^|[-_])([a-z])/g, (_m, _s, c) => c.toUp
|
|
|
23738
24085
|
`
|
|
23739
24086
|
});
|
|
23740
24087
|
}
|
|
24088
|
+
const order = [];
|
|
24089
|
+
if (sections) {
|
|
24090
|
+
order.push(
|
|
24091
|
+
`1. Create the controls the plan names (${Object.values(ids).slice(0, shape === "create" ? 3 : 1).join(", ")}), with stable locators. inspect_page gives you candidates.`
|
|
24092
|
+
);
|
|
24093
|
+
if (shape === "create") {
|
|
24094
|
+
order.push(`2. Create the form ${ids.form}, naming those controls as entry, submit and required fields.`);
|
|
24095
|
+
}
|
|
24096
|
+
}
|
|
24097
|
+
order.push(
|
|
24098
|
+
options.clientSide ? "Register the client action in the page. It runs in the user's session, so it needs no credential." : "Create the tool. path_template is a path on the host origin, not an absolute URL, and the executor rejects an absolute one. If it calls an authenticated backend, set auth_secret in the same call, because a tool with no stored credential runs in the page and cannot be used as session preflight."
|
|
24099
|
+
);
|
|
24100
|
+
if (sections) {
|
|
24101
|
+
order.push("Create the action plan. Run validate_action_plan on the sections first.");
|
|
24102
|
+
}
|
|
24103
|
+
order.push("Create the knowledge article, and keep the procedure out of it.");
|
|
24104
|
+
order.push("Activate the plan and the article once validate_config is clean.");
|
|
24105
|
+
const notes = [
|
|
24106
|
+
"The test for agent-first is whether a user could complete this capability end to end through the assistant alone. If not, it has a screen and nothing else.",
|
|
24107
|
+
"Both the plan description and the tool description are how the agent finds them. Write the trigger, in the user's words, and localize it.",
|
|
24108
|
+
"is_active is false on the plan and the article on purpose. Turn them on after validate_config passes, not before."
|
|
24109
|
+
];
|
|
24110
|
+
if (shape === "read") {
|
|
24111
|
+
notes.push(
|
|
24112
|
+
"No action plan is emitted for a read capability. A plan whose only step opens a screen does nothing, and the health contract reports it. The tool answers the question and the knowledge article explains what the answer means."
|
|
24113
|
+
);
|
|
24114
|
+
}
|
|
24115
|
+
if (options.clientSide) {
|
|
24116
|
+
notes.push(
|
|
24117
|
+
"No HTTP-proxy tool is emitted, because the operation runs in the page. Add one only if a second, server-authorized path to the same capability is genuinely needed."
|
|
24118
|
+
);
|
|
24119
|
+
}
|
|
24120
|
+
notes.push(
|
|
24121
|
+
`The plan's steps run on view_path "${viewPath}". Pass viewPath when the screen is somewhere else; the value must match a registered View.`
|
|
24122
|
+
);
|
|
24123
|
+
return { capability, shape, tool, actionPlan, knowledge, files, order, notes };
|
|
24124
|
+
}
|
|
24125
|
+
function integrationSnippet(options) {
|
|
24126
|
+
const idiom = publicKeyIdiom(options.framework ?? "other");
|
|
24127
|
+
const keyAttr = options.widgetKey ? `
|
|
24128
|
+
data-api-key="${options.widgetKey}"` : "";
|
|
24129
|
+
const apiAttr = options.apiUrl ? `
|
|
24130
|
+
data-api-url="${options.apiUrl}"` : "";
|
|
24131
|
+
const bootLines = ["import { bootAppilotWidget } from 'appilot';", "", "bootAppilotWidget({"];
|
|
24132
|
+
bootLines.push(` widgetScriptUrl: '${options.widgetScriptUrl}',`);
|
|
24133
|
+
bootLines.push(` widgetKey: ${idiom.expr},`);
|
|
24134
|
+
if (options.apiUrl) bootLines.push(` appilotApiUrl: '${options.apiUrl}',`);
|
|
24135
|
+
bootLines.push(" tokenEndpoint: '/api/widget/token',", "});");
|
|
23741
24136
|
return {
|
|
23742
|
-
|
|
23743
|
-
|
|
23744
|
-
|
|
23745
|
-
|
|
23746
|
-
|
|
23747
|
-
|
|
23748
|
-
|
|
23749
|
-
|
|
23750
|
-
"3. Create the tool. path_template is a path on the host origin, not an absolute URL, and the executor rejects an absolute one. If it calls an authenticated backend, set auth_secret in the same call, because a tool with no stored credential runs in the page and cannot be used as session preflight.",
|
|
23751
|
-
"4. Create the action plan. Run validate_action_plan first; a plan that only opens an element does nothing.",
|
|
23752
|
-
"5. Create the knowledge article, and keep the procedure out of it.",
|
|
23753
|
-
"6. Activate the plan and the article once validate_config is clean."
|
|
23754
|
-
],
|
|
24137
|
+
apiUrl: options.apiUrl ?? null,
|
|
24138
|
+
scriptTag: `<script
|
|
24139
|
+
src="${options.widgetScriptUrl}"${keyAttr}${apiAttr}
|
|
24140
|
+
async
|
|
24141
|
+
></script>`,
|
|
24142
|
+
bootSnippet: bootLines.join("\n"),
|
|
24143
|
+
tokenEndpointHint: "Serve /api/widget/token from your backend with createWidgetTokenHandler from appilot-server. The widget requires an identified user, and the widget secret must never reach a browser.",
|
|
24144
|
+
publicEnvName: idiom.envName,
|
|
23755
24145
|
notes: [
|
|
23756
|
-
"The
|
|
23757
|
-
|
|
23758
|
-
"is_active is false on the plan and the article on purpose. Turn them on after validate_config passes, not before."
|
|
24146
|
+
"The script tag is the no-build form and carries the key as an attribute. The boot call is the bundled form and reads it from the public variable, which is what survives a key rotation.",
|
|
24147
|
+
...localDevelopmentNotes(idiom)
|
|
23759
24148
|
]
|
|
23760
24149
|
};
|
|
23761
24150
|
}
|
|
23762
|
-
var CLIENT_ACTION_EXAMPLE;
|
|
24151
|
+
var SCAFFOLD_FRAMEWORKS, CLIENT_ACTION_EXAMPLE;
|
|
23763
24152
|
var init_scaffold = __esm({
|
|
23764
24153
|
"src/scaffold.ts"() {
|
|
23765
24154
|
"use strict";
|
|
24155
|
+
init_validation();
|
|
24156
|
+
SCAFFOLD_FRAMEWORKS = [
|
|
24157
|
+
"next",
|
|
24158
|
+
"express",
|
|
24159
|
+
"fastify",
|
|
24160
|
+
"hono",
|
|
24161
|
+
"remix",
|
|
24162
|
+
"sveltekit",
|
|
24163
|
+
"other"
|
|
24164
|
+
];
|
|
23766
24165
|
CLIENT_ACTION_EXAMPLE = `import { registerTool } from 'appilot';
|
|
23767
24166
|
|
|
23768
24167
|
// A client action is an operation your PAGE performs, in the user's own
|
|
@@ -24045,7 +24444,7 @@ function createAppilotServer(conn) {
|
|
|
24045
24444
|
if (!granted || granted.includes(scope)) return null;
|
|
24046
24445
|
return errorText(
|
|
24047
24446
|
new Error(
|
|
24048
|
-
`This connection was granted ${granted.length ? granted.join(", ") : "no scopes"}, which does not include ${scope}. Reconnect and approve that scope, using a service token that carries it
|
|
24447
|
+
`This connection was granted ${granted.length ? granted.join(", ") : "no scopes"}, which does not include ${scope}. Reconnect and approve that scope, using a service token that carries it. ` + HOW_TO_GRANT[scope]
|
|
24049
24448
|
)
|
|
24050
24449
|
);
|
|
24051
24450
|
}
|
|
@@ -24327,24 +24726,25 @@ ${JSON.stringify(bundle, null, 2)}`);
|
|
|
24327
24726
|
"create_app",
|
|
24328
24727
|
{
|
|
24329
24728
|
title: "Create an app, its domains, and a widget key",
|
|
24330
|
-
description: "Provision an Appilot app in one call: the app, the domains it runs on, and optionally a widget key, plus the exact script tag and boot snippet to paste into the host application. Idempotent: re-running converges on the existing app rather than creating a second one. Pass dryRun to preview. The widget key and its secret are returned EXACTLY ONCE, at creation; store the secret in the host backend only. Requires a provision:write service token.",
|
|
24729
|
+
description: "Provision an Appilot app in one call: the app, the domains it runs on, and optionally a widget key, plus the exact script tag and boot snippet to paste into the host application. Idempotent: re-running converges on the existing app rather than creating a second one. Pass dryRun to preview. The widget key and its secret are returned EXACTLY ONCE, at creation; store the secret in the host backend only. Working on your own machine: do NOT pass localhost or 127.0.0.1 as a domain, because they name every developer's machine and are refused. Either run the app on a hostname that resolves to 127.0.0.1 (myapp.lvh.me) and register THAT, which gives the turn full app context and needs no key, or pass isTest true with no domains for a loopback-bound wk_test_ key, which gives the tenant and the user and no app context. Requires a provision:write service token.",
|
|
24331
24730
|
inputSchema: {
|
|
24332
24731
|
name: external_exports.string().min(1),
|
|
24333
24732
|
description: external_exports.string().optional(),
|
|
24334
24733
|
domains: external_exports.array(external_exports.string()).optional(),
|
|
24335
24734
|
widgetKeyName: external_exports.string().optional(),
|
|
24336
|
-
|
|
24735
|
+
isTest: external_exports.boolean().optional(),
|
|
24736
|
+
allowedDomains: external_exports.array(external_exports.string()).optional(),
|
|
24337
24737
|
dryRun: external_exports.boolean().optional()
|
|
24338
24738
|
}
|
|
24339
24739
|
},
|
|
24340
|
-
async ({ name, description, domains, widgetKeyName,
|
|
24740
|
+
async ({ name, description, domains, widgetKeyName, isTest, allowedDomains, dryRun }) => {
|
|
24341
24741
|
const refusal = scopeRefusal("provision:write");
|
|
24342
24742
|
if (refusal) return refusal;
|
|
24343
24743
|
try {
|
|
24344
24744
|
const result = await client.provisionApp({
|
|
24345
24745
|
app: { name, description },
|
|
24346
24746
|
domains: (domains ?? []).map((domain) => ({ domain })),
|
|
24347
|
-
widgetKey: widgetKeyName ||
|
|
24747
|
+
widgetKey: widgetKeyName || isTest !== void 0 || allowedDomains ? { name: widgetKeyName, isTest, allowedDomains } : void 0,
|
|
24348
24748
|
dryRun: dryRun === true
|
|
24349
24749
|
});
|
|
24350
24750
|
return text(redactForTransport(result, conn.transport));
|
|
@@ -24353,11 +24753,138 @@ ${JSON.stringify(bundle, null, 2)}`);
|
|
|
24353
24753
|
}
|
|
24354
24754
|
}
|
|
24355
24755
|
);
|
|
24756
|
+
server.registerTool(
|
|
24757
|
+
"list_apps",
|
|
24758
|
+
{
|
|
24759
|
+
title: "List the apps this organization has provisioned",
|
|
24760
|
+
description: `The apps this credential reaches, with each app's registered domains and their verification status. Call it before create_app so "which apps do I have" has an answer, and to find the app id and the domain id every other provisioning tool takes. Carries no secret. Reads the provisioning surface, so it needs a provision:write service token even though it writes nothing.`,
|
|
24761
|
+
inputSchema: {}
|
|
24762
|
+
},
|
|
24763
|
+
async () => {
|
|
24764
|
+
const refusal = scopeRefusal("provision:write");
|
|
24765
|
+
if (refusal) return refusal;
|
|
24766
|
+
try {
|
|
24767
|
+
return text(await client.listProvisioned());
|
|
24768
|
+
} catch (err) {
|
|
24769
|
+
return errorText(err);
|
|
24770
|
+
}
|
|
24771
|
+
}
|
|
24772
|
+
);
|
|
24773
|
+
server.registerTool(
|
|
24774
|
+
"list_widget_keys",
|
|
24775
|
+
{
|
|
24776
|
+
title: "List the widget keys this organization holds",
|
|
24777
|
+
description: "The organization's widget keys by label, prefix, allowed domains and active state. No raw key and no secret: both are shown exactly once, at creation. Call it before create_app so a re-run recognises the key it already minted instead of asking for another, because every extra key is another live credential. Requires a provision:write service token.",
|
|
24778
|
+
inputSchema: {}
|
|
24779
|
+
},
|
|
24780
|
+
async () => {
|
|
24781
|
+
const refusal = scopeRefusal("provision:write");
|
|
24782
|
+
if (refusal) return refusal;
|
|
24783
|
+
try {
|
|
24784
|
+
return text(await client.listWidgetKeys());
|
|
24785
|
+
} catch (err) {
|
|
24786
|
+
return errorText(err);
|
|
24787
|
+
}
|
|
24788
|
+
}
|
|
24789
|
+
);
|
|
24790
|
+
server.registerTool(
|
|
24791
|
+
"verify_domain",
|
|
24792
|
+
{
|
|
24793
|
+
title: "Check or trigger a domain's DNS verification",
|
|
24794
|
+
description: "Where a domain's verification stands, and the exact TXT record it needs. Pass trigger to ask Appilot to look for the record now. A live widget key needs a verified domain, and this is what closes that loop: create_app reports the key as blocked and names the record, and this reports whether the record is visible yet. Requires a provision:write service token.",
|
|
24795
|
+
inputSchema: {
|
|
24796
|
+
domain: external_exports.string().min(1),
|
|
24797
|
+
appId: external_exports.number().int().optional(),
|
|
24798
|
+
trigger: external_exports.boolean().optional()
|
|
24799
|
+
}
|
|
24800
|
+
},
|
|
24801
|
+
async ({ domain, appId, trigger }) => {
|
|
24802
|
+
const refusal = scopeRefusal("provision:write");
|
|
24803
|
+
if (refusal) return refusal;
|
|
24804
|
+
try {
|
|
24805
|
+
const wanted = domain.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/[/:].*$/, "");
|
|
24806
|
+
const provisioned = await client.listProvisioned();
|
|
24807
|
+
const scoped = appId != null ? provisioned.filter((a) => Number(a.id) === appId) : provisioned;
|
|
24808
|
+
const owner = scoped.find((a) => a.domains.some((d) => String(d.domain).toLowerCase() === wanted));
|
|
24809
|
+
const row = owner?.domains.find((d) => String(d.domain).toLowerCase() === wanted);
|
|
24810
|
+
if (!owner || !row) {
|
|
24811
|
+
return errorText(new Error(
|
|
24812
|
+
`${wanted} is not a registered domain of ${appId != null ? `app ${appId}` : "any app in this organization"}. list_apps shows what is registered, and create_app adds a domain. A loopback name (localhost, 127.0.0.1) is never registrable: run the app on a hostname that resolves to 127.0.0.1 instead.`
|
|
24813
|
+
));
|
|
24814
|
+
}
|
|
24815
|
+
const result = {
|
|
24816
|
+
app: { id: owner.id, name: owner.name },
|
|
24817
|
+
domain: row.domain,
|
|
24818
|
+
domainId: row.id,
|
|
24819
|
+
verificationStatus: row.verification_status
|
|
24820
|
+
};
|
|
24821
|
+
try {
|
|
24822
|
+
result.record = await client.domainVerification(Number(row.id));
|
|
24823
|
+
} catch {
|
|
24824
|
+
try {
|
|
24825
|
+
const preview = await client.provisionApp({
|
|
24826
|
+
app: { name: owner.name },
|
|
24827
|
+
domains: [{ domain: row.domain }],
|
|
24828
|
+
dryRun: true
|
|
24829
|
+
});
|
|
24830
|
+
const previewed = preview.domains.find((d) => d.domain.toLowerCase() === wanted);
|
|
24831
|
+
if (previewed?.dns_record_name) {
|
|
24832
|
+
result.record = {
|
|
24833
|
+
dns_record_name: previewed.dns_record_name,
|
|
24834
|
+
dns_record_value: previewed.dns_record_value,
|
|
24835
|
+
verification_status: previewed.verification_status
|
|
24836
|
+
};
|
|
24837
|
+
}
|
|
24838
|
+
} catch (err) {
|
|
24839
|
+
result.recordUnavailable = err instanceof Error ? err.message : String(err);
|
|
24840
|
+
}
|
|
24841
|
+
}
|
|
24842
|
+
if (trigger && row.verification_status !== "verified") {
|
|
24843
|
+
try {
|
|
24844
|
+
result.checked = await client.triggerDomainVerification(Number(row.id));
|
|
24845
|
+
} catch (err) {
|
|
24846
|
+
result.triggerRefused = `${err instanceof Error ? err.message : String(err)} Triggering the check authenticates an organization session rather than a service token today, so run it from the Backoffice under Domains once the TXT record is published.`;
|
|
24847
|
+
}
|
|
24848
|
+
}
|
|
24849
|
+
return text(result);
|
|
24850
|
+
} catch (err) {
|
|
24851
|
+
return errorText(err);
|
|
24852
|
+
}
|
|
24853
|
+
}
|
|
24854
|
+
);
|
|
24855
|
+
server.registerTool(
|
|
24856
|
+
"integration_snippet",
|
|
24857
|
+
{
|
|
24858
|
+
title: "Get the script tag and boot call for an app that already exists",
|
|
24859
|
+
description: "The two forms of the widget boot for an app you already provisioned: the no-build script tag, and the bundled bootAppilotWidget call reading the key from the framework's public variable. Use it instead of re-running create_app, which is a provisioning write, when all you lost was the snippet. It composes the answer locally, writes nothing, and needs no scope. Pass the publishable widget key to have it appear in the script tag; the widget SECRET never belongs here.",
|
|
24860
|
+
inputSchema: {
|
|
24861
|
+
appId: external_exports.number().int().optional(),
|
|
24862
|
+
widgetKey: external_exports.string().optional(),
|
|
24863
|
+
widgetScriptUrl: external_exports.string().optional(),
|
|
24864
|
+
framework: external_exports.enum(SCAFFOLD_FRAMEWORKS).optional()
|
|
24865
|
+
}
|
|
24866
|
+
},
|
|
24867
|
+
async ({ appId, widgetKey, widgetScriptUrl, framework }) => {
|
|
24868
|
+
try {
|
|
24869
|
+
return text({
|
|
24870
|
+
appId: appId ?? conn.defaultAppId ?? null,
|
|
24871
|
+
...integrationSnippet({
|
|
24872
|
+
widgetScriptUrl: widgetScriptUrl ?? DEFAULT_WIDGET_SCRIPT_URL,
|
|
24873
|
+
apiUrl: conn.baseUrl || null,
|
|
24874
|
+
widgetKey: widgetKey ?? null,
|
|
24875
|
+
framework
|
|
24876
|
+
})
|
|
24877
|
+
});
|
|
24878
|
+
} catch (err) {
|
|
24879
|
+
return errorText(err);
|
|
24880
|
+
}
|
|
24881
|
+
}
|
|
24882
|
+
);
|
|
24356
24883
|
server.registerTool(
|
|
24357
24884
|
"plan_manifest",
|
|
24358
24885
|
{
|
|
24359
24886
|
title: "Preview an app manifest (writes nothing)",
|
|
24360
|
-
description: "Diff an appilot.app-manifest against the live instance and return what would change: provisioning actions per app/domain/key, the config-bundle import diff, and the health findings over the resulting state. Writes nothing. Returns a planToken that apply_manifest requires, so an apply always follows a preview of the exact same manifest. Keep the manifest in the repository under version control.",
|
|
24887
|
+
description: "Diff an appilot.app-manifest against the live instance and return what would change: provisioning actions per app/domain/key, the config-bundle import diff, and the health findings over the resulting state. Writes nothing. Returns a planToken that apply_manifest requires, so an apply always follows a preview of the exact same manifest. Keep the manifest in the repository under version control. A complete example ships with this package at examples/app.appilot.json and is also in the docs. Which half is revisioned: the CONFIG half gets a pre_restore revision on every commit, so a wrong import is undone by restoring it; the PROVISIONING half is not revisioned, so a domain or a key it creates is undone by hand.",
|
|
24361
24888
|
inputSchema: { manifest: external_exports.union([external_exports.record(external_exports.any()), external_exports.string()]) }
|
|
24362
24889
|
},
|
|
24363
24890
|
async ({ manifest }) => {
|
|
@@ -24379,7 +24906,7 @@ ${JSON.stringify(bundle, null, 2)}`);
|
|
|
24379
24906
|
"apply_manifest",
|
|
24380
24907
|
{
|
|
24381
24908
|
title: "Apply a previously planned app manifest",
|
|
24382
|
-
description: "Provision and configure an app from an appilot.app-manifest. Requires the planToken returned by plan_manifest for the SAME manifest: a mismatch means the manifest changed after it was previewed, and the apply is refused. Pass expectedCurrentHash from the plan so a concurrent config edit is a 409 rather than a silent overwrite. mode=replace makes the config match the bundle exactly, including deletions.
|
|
24909
|
+
description: "Provision and configure an app from an appilot.app-manifest. Requires the planToken returned by plan_manifest for the SAME manifest: a mismatch means the manifest changed after it was previewed, and the apply is refused. Pass expectedCurrentHash from the plan so a concurrent config edit is a 409 rather than a silent overwrite. mode=replace makes the config match the bundle exactly, including deletions. Needs config:write when the manifest carries a config bundle, and provision:write only when the provisioning half would actually change something: a manifest whose app, domains and keys already exist applies with config:write alone.",
|
|
24383
24910
|
inputSchema: {
|
|
24384
24911
|
manifest: external_exports.union([external_exports.record(external_exports.any()), external_exports.string()]),
|
|
24385
24912
|
planToken: external_exports.string(),
|
|
@@ -24389,15 +24916,19 @@ ${JSON.stringify(bundle, null, 2)}`);
|
|
|
24389
24916
|
}
|
|
24390
24917
|
},
|
|
24391
24918
|
async ({ manifest, planToken, mode, expectedCurrentHash, allowUnhealthy }) => {
|
|
24392
|
-
const refusal = scopeRefusal("provision:write") ?? scopeRefusal("config:write");
|
|
24393
|
-
if (refusal) return refusal;
|
|
24394
24919
|
try {
|
|
24395
24920
|
const parsed = parseManifest(manifest);
|
|
24921
|
+
if (parsed.config) {
|
|
24922
|
+
const configRefusal = scopeRefusal("config:write");
|
|
24923
|
+
if (configRefusal) return configRefusal;
|
|
24924
|
+
}
|
|
24925
|
+
const provisionRefusal = scopeRefusal("provision:write");
|
|
24396
24926
|
const result = await applyManifest(client, parsed, {
|
|
24397
24927
|
planToken,
|
|
24398
24928
|
mode,
|
|
24399
24929
|
expectedCurrentHash,
|
|
24400
|
-
allowUnhealthy
|
|
24930
|
+
allowUnhealthy,
|
|
24931
|
+
provisionRefusal: provisionRefusal ? provisionRefusal.content[0].text : null
|
|
24401
24932
|
});
|
|
24402
24933
|
return text({ ...result, provisioning: redactForTransport(result.provisioning, conn.transport) });
|
|
24403
24934
|
} catch (err) {
|
|
@@ -24409,9 +24940,9 @@ ${JSON.stringify(bundle, null, 2)}`);
|
|
|
24409
24940
|
"scaffold_integration",
|
|
24410
24941
|
{
|
|
24411
24942
|
title: "Generate the host application integration code",
|
|
24412
|
-
description: "Return the source a host application needs: the identity relay for its backend (the one security-critical piece, built on appilot-server), the widget boot call, and a client-action example. Returns file CONTENTS for you to write into the repository; this server never touches the filesystem. Pick the framework that matches the host.",
|
|
24943
|
+
description: "Return the source a host application needs: the identity relay for its backend (the one security-critical piece, built on appilot-server), the widget boot call, and a client-action example. Returns file CONTENTS for you to write into the repository; this server never touches the filesystem. Pick the framework that matches the host, or `other` when the backend is not Node (Django, Rails, PHP), which returns the raw exchange as curl plus a Python and a Ruby handler. The notes carry what local development needs, and the answer differs between a hostname you registered and bare localhost.",
|
|
24413
24944
|
inputSchema: {
|
|
24414
|
-
framework: external_exports.enum(
|
|
24945
|
+
framework: external_exports.enum(SCAFFOLD_FRAMEWORKS),
|
|
24415
24946
|
widgetKey: external_exports.string().optional(),
|
|
24416
24947
|
widgetScriptUrl: external_exports.string().optional(),
|
|
24417
24948
|
idNamespace: external_exports.string().optional()
|
|
@@ -24499,16 +25030,18 @@ ${JSON.stringify(bundle, null, 2)}`);
|
|
|
24499
25030
|
"scaffold_agent_first",
|
|
24500
25031
|
{
|
|
24501
25032
|
title: "Scaffold one capability so the agent can operate it",
|
|
24502
|
-
description: "Return the four artifacts a capability needs to be agent-operable, agreeing with each other: the HTTP-proxy tool, the client action when the operation belongs in the page, the Action Plan that is the procedure, and the knowledge article that carries the meaning and not the steps. Also returns the order to create them in, which matters because a form cannot name controls that do not exist yet. `endpoint.path` is a path on the host origin, not an absolute URL. Use it when building a new agent-first app or making an existing feature reachable through the assistant.",
|
|
25033
|
+
description: "Return the four artifacts a capability needs to be agent-operable, agreeing with each other: the HTTP-proxy tool, the client action when the operation belongs in the page, the Action Plan that is the procedure, and the knowledge article that carries the meaning and not the steps. Also returns the order to create them in, which matters because a form cannot name controls that do not exist yet. `endpoint.path` is a path on the host origin, not an absolute URL. Pass `shape`: `create` is the open, fill and submit plan, `navigate` is one step to a screen, and `read` gets NO plan at all, because a plan whose only step opens a screen does nothing. Pass `clientSide` when the operation belongs in the page, and no HTTP-proxy tool is emitted. Use it when building a new agent-first app or making an existing feature reachable through the assistant.",
|
|
24503
25034
|
inputSchema: {
|
|
24504
25035
|
capability: external_exports.string().min(1),
|
|
24505
25036
|
slug: external_exports.string().min(1),
|
|
24506
25037
|
appId: external_exports.number().int().optional(),
|
|
24507
25038
|
endpoint: external_exports.object({ method: external_exports.string(), path: external_exports.string() }).optional(),
|
|
24508
|
-
clientSide: external_exports.boolean().optional()
|
|
25039
|
+
clientSide: external_exports.boolean().optional(),
|
|
25040
|
+
shape: external_exports.enum(["create", "navigate", "read"]).optional(),
|
|
25041
|
+
viewPath: external_exports.string().optional()
|
|
24509
25042
|
}
|
|
24510
25043
|
},
|
|
24511
|
-
async ({ capability, slug, appId, endpoint, clientSide }) => {
|
|
25044
|
+
async ({ capability, slug, appId, endpoint, clientSide, shape, viewPath }) => {
|
|
24512
25045
|
try {
|
|
24513
25046
|
return text(
|
|
24514
25047
|
scaffoldAgentFirst({
|
|
@@ -24516,7 +25049,9 @@ ${JSON.stringify(bundle, null, 2)}`);
|
|
|
24516
25049
|
slug,
|
|
24517
25050
|
appId: appId ?? conn.defaultAppId ?? null,
|
|
24518
25051
|
endpoint: endpoint ?? null,
|
|
24519
|
-
clientSide
|
|
25052
|
+
clientSide,
|
|
25053
|
+
shape,
|
|
25054
|
+
viewPath
|
|
24520
25055
|
})
|
|
24521
25056
|
);
|
|
24522
25057
|
} catch (err) {
|
|
@@ -24588,7 +25123,7 @@ ${JSON.stringify(bundle, null, 2)}`);
|
|
|
24588
25123
|
);
|
|
24589
25124
|
return server;
|
|
24590
25125
|
}
|
|
24591
|
-
var DEFAULT_WIDGET_SCRIPT_URL, SERVER_INSTRUCTIONS;
|
|
25126
|
+
var DEFAULT_WIDGET_SCRIPT_URL, SERVER_INSTRUCTIONS, HOW_TO_GRANT;
|
|
24592
25127
|
var init_server3 = __esm({
|
|
24593
25128
|
"src/server.ts"() {
|
|
24594
25129
|
"use strict";
|
|
@@ -24607,7 +25142,9 @@ var init_server3 = __esm({
|
|
|
24607
25142
|
DEFAULT_WIDGET_SCRIPT_URL = "https://cdn.appilot.space/widget/v1/appilot.esm.js";
|
|
24608
25143
|
SERVER_INSTRUCTIONS = `Audit, extend and fix an Appilot app's content-model configuration, and build a new capability so the agent can operate it.
|
|
24609
25144
|
|
|
24610
|
-
|
|
25145
|
+
For a new app, work in this order: whoami, then create_app (on bare localhost pass isTest, which mints a wk_test_ key), then scaffold_integration for the host's framework, then verify_integration against the running page, then configure the content model.
|
|
25146
|
+
|
|
25147
|
+
To audit an existing one, work in this order: capabilities (what this instance supports, and the closed vocabularies its entities accept; on-premise trails cloud), read_config, validate_config, then report the findings to the user in plain language, ranked critical to low, each with its concrete fix. Do not paste raw tool output at them.
|
|
24611
25148
|
|
|
24612
25149
|
If read_config returns a gaps array, an entity could not be read. Say so and stop treating that entity as empty: every lint over it silently passed.
|
|
24613
25150
|
|
|
@@ -24622,6 +25159,12 @@ Three things to get right. A widget secret belongs in the server environment and
|
|
|
24622
25159
|
If something is missing or broken in Appilot itself, report_feedback records it. Say plainly what it does: the report is read, and a reply is part of a support plan rather than something promised here. Never put configuration contents, knowledge bodies or secrets in a report, and show the user the exact text first.
|
|
24623
25160
|
|
|
24624
25161
|
Writing needs a config:write service token, reading needs config:read, provisioning needs provision:write, reporting needs feedback:write. A scope refusal means the user should reconnect with a token carrying that scope, not that you should find another route.`;
|
|
25162
|
+
HOW_TO_GRANT = {
|
|
25163
|
+
"config:read": 'config:read is on every Service tokens preset in the Backoffice, "Inspect only" included.',
|
|
25164
|
+
"config:write": 'config:write comes from the Backoffice Service tokens preset "Edit configuration".',
|
|
25165
|
+
"provision:write": 'provision:write is granted by an organization administrator in the Backoffice, under Service tokens with the "Set up integrations" preset, or through the account approval screen. A configurator who is not an administrator cannot mint it and has to ask one.',
|
|
25166
|
+
"feedback:write": "feedback:write is a Backoffice Service tokens option, and it is the only scope that sends anything out of the organization."
|
|
25167
|
+
};
|
|
24625
25168
|
}
|
|
24626
25169
|
});
|
|
24627
25170
|
|
|
@@ -43475,7 +44018,7 @@ var require_view = __commonJS({
|
|
|
43475
44018
|
var dirname = path.dirname;
|
|
43476
44019
|
var basename = path.basename;
|
|
43477
44020
|
var extname = path.extname;
|
|
43478
|
-
var
|
|
44021
|
+
var join2 = path.join;
|
|
43479
44022
|
var resolve = path.resolve;
|
|
43480
44023
|
module.exports = View;
|
|
43481
44024
|
function View(name, options) {
|
|
@@ -43537,12 +44080,12 @@ var require_view = __commonJS({
|
|
|
43537
44080
|
};
|
|
43538
44081
|
View.prototype.resolve = function resolve2(dir, file) {
|
|
43539
44082
|
var ext = this.ext;
|
|
43540
|
-
var path2 =
|
|
44083
|
+
var path2 = join2(dir, file);
|
|
43541
44084
|
var stat = tryStat(path2);
|
|
43542
44085
|
if (stat && stat.isFile()) {
|
|
43543
44086
|
return path2;
|
|
43544
44087
|
}
|
|
43545
|
-
path2 =
|
|
44088
|
+
path2 = join2(dir, basename(file, ext), "index" + ext);
|
|
43546
44089
|
stat = tryStat(path2);
|
|
43547
44090
|
if (stat && stat.isFile()) {
|
|
43548
44091
|
return path2;
|
|
@@ -47350,7 +47893,7 @@ var require_send = __commonJS({
|
|
|
47350
47893
|
var Stream = __require("stream");
|
|
47351
47894
|
var util2 = __require("util");
|
|
47352
47895
|
var extname = path.extname;
|
|
47353
|
-
var
|
|
47896
|
+
var join2 = path.join;
|
|
47354
47897
|
var normalize = path.normalize;
|
|
47355
47898
|
var resolve = path.resolve;
|
|
47356
47899
|
var sep = path.sep;
|
|
@@ -47522,7 +48065,7 @@ var require_send = __commonJS({
|
|
|
47522
48065
|
return res;
|
|
47523
48066
|
}
|
|
47524
48067
|
parts = path2.split(sep);
|
|
47525
|
-
path2 = normalize(
|
|
48068
|
+
path2 = normalize(join2(root, path2));
|
|
47526
48069
|
} else {
|
|
47527
48070
|
if (UP_PATH_REGEXP.test(path2)) {
|
|
47528
48071
|
debug('malicious path "%s"', path2);
|
|
@@ -47655,7 +48198,7 @@ var require_send = __commonJS({
|
|
|
47655
48198
|
if (err) return self.onStatError(err);
|
|
47656
48199
|
return self.error(404);
|
|
47657
48200
|
}
|
|
47658
|
-
var p =
|
|
48201
|
+
var p = join2(path2, self._index[i]);
|
|
47659
48202
|
debug('stat "%s"', p);
|
|
47660
48203
|
fs.stat(p, function(err2, stat) {
|
|
47661
48204
|
if (err2) return next(err2);
|
|
@@ -53493,10 +54036,6 @@ function message(key, locale = "en", values = {}) {
|
|
|
53493
54036
|
const template = ({ en, es, de }[locale] ?? en)[key];
|
|
53494
54037
|
return template.replace(/\{(\w+)\}/g, (_, name) => values[name] ?? `{${name}}`);
|
|
53495
54038
|
}
|
|
53496
|
-
function translateError(text, locale) {
|
|
53497
|
-
const entry = Object.entries(en).find(([, value]) => value === text);
|
|
53498
|
-
return entry ? message(entry[0], locale) : message("unavailableDetail", locale);
|
|
53499
|
-
}
|
|
53500
54039
|
var en, es, de;
|
|
53501
54040
|
var init_consentMessages = __esm({
|
|
53502
54041
|
"src/remote/consentMessages.ts"() {
|
|
@@ -53556,7 +54095,12 @@ var init_consentMessages = __esm({
|
|
|
53556
54095
|
failed: "Connection could not be completed",
|
|
53557
54096
|
failedHelp: "The approval expired, was already used, or belongs to another browser. Start again from your assistant.",
|
|
53558
54097
|
unavailableTitle: "Connection unavailable",
|
|
53559
|
-
unavailableDetail: "Appilot could not complete the connection. Please try again."
|
|
54098
|
+
unavailableDetail: "Appilot could not complete the connection. Please try again.",
|
|
54099
|
+
// What a client is called when it did not register a name. One constant, so
|
|
54100
|
+
// the same connection is not "your assistant" on one screen and "an MCP
|
|
54101
|
+
// client" on the next.
|
|
54102
|
+
clientFallback: "your assistant",
|
|
54103
|
+
appNumber: "App #{id}"
|
|
53560
54104
|
};
|
|
53561
54105
|
es = {
|
|
53562
54106
|
sessionUnavailable: "La aprobaci\xF3n desde tu cuenta no est\xE1 disponible temporalmente. Intenta conectar otra vez o usa un token de servicio abajo.",
|
|
@@ -53613,7 +54157,9 @@ var init_consentMessages = __esm({
|
|
|
53613
54157
|
failed: "No se pudo completar la conexi\xF3n",
|
|
53614
54158
|
failedHelp: "La aprobaci\xF3n venci\xF3, ya se utiliz\xF3 o pertenece a otro navegador. Vuelve a empezar desde tu asistente.",
|
|
53615
54159
|
unavailableTitle: "Conexi\xF3n no disponible",
|
|
53616
|
-
unavailableDetail: "Appilot no pudo completar la conexi\xF3n. Int\xE9ntalo de nuevo."
|
|
54160
|
+
unavailableDetail: "Appilot no pudo completar la conexi\xF3n. Int\xE9ntalo de nuevo.",
|
|
54161
|
+
clientFallback: "tu asistente",
|
|
54162
|
+
appNumber: "Aplicaci\xF3n n.\xBA {id}"
|
|
53617
54163
|
};
|
|
53618
54164
|
de = {
|
|
53619
54165
|
sessionUnavailable: "Die Freigabe \xFCber Ihr Konto ist vor\xFCbergehend nicht verf\xFCgbar. Starten Sie erneut oder verwenden Sie unten ein Service-Token.",
|
|
@@ -53670,7 +54216,9 @@ var init_consentMessages = __esm({
|
|
|
53670
54216
|
failed: "Verbindung konnte nicht abgeschlossen werden",
|
|
53671
54217
|
failedHelp: "Die Freigabe ist abgelaufen, wurde bereits verwendet oder geh\xF6rt zu einem anderen Browser. Starten Sie erneut in Ihrem Assistenten.",
|
|
53672
54218
|
unavailableTitle: "Verbindung nicht verf\xFCgbar",
|
|
53673
|
-
unavailableDetail: "Appilot konnte die Verbindung nicht abschlie\xDFen. Versuchen Sie es erneut."
|
|
54219
|
+
unavailableDetail: "Appilot konnte die Verbindung nicht abschlie\xDFen. Versuchen Sie es erneut.",
|
|
54220
|
+
clientFallback: "Ihr Assistent",
|
|
54221
|
+
appNumber: "App Nr. {id}"
|
|
53674
54222
|
};
|
|
53675
54223
|
}
|
|
53676
54224
|
});
|
|
@@ -53721,10 +54269,10 @@ function renderConfirmPage(opts) {
|
|
|
53721
54269
|
const locale = opts.locale ?? "en";
|
|
53722
54270
|
const m = (key, values) => escapeHtml(message(key, locale, values));
|
|
53723
54271
|
const organization = opts.organizationName || (opts.organizationId != null ? `${message("organization", locale)} #${opts.organizationId}` : message("unknown", locale));
|
|
53724
|
-
const app = opts.appName || (opts.appId != null ?
|
|
54272
|
+
const app = opts.appName || (opts.appId != null ? message("appNumber", locale, { id: String(opts.appId) }) : message("allApps", locale));
|
|
53725
54273
|
return page(
|
|
53726
54274
|
message("review", locale),
|
|
53727
|
-
`${steps(2, locale)}<h1>${m("reviewFor", { client: opts.clientName || "
|
|
54275
|
+
`${steps(2, locale)}<h1>${m("reviewFor", { client: opts.clientName || message("clientFallback", locale) })}</h1><p>${m("accepted")}</p>
|
|
53728
54276
|
<div class="panel"><dl><div><dt>${m("organization")}</dt><dd>${escapeHtml(organization)}</dd></div><div><dt>${m(opts.appScoped ? "limited" : "appAccess")}</dt><dd>${escapeHtml(app)}</dd></div></dl>${opts.appId != null && !opts.appScoped ? `<small>${m("defaultNote")}</small>` : ""}</div>
|
|
53729
54277
|
<h2>${m("can")}</h2>${scopeItems(opts.scopes, locale)}
|
|
53730
54278
|
${opts.unavailableScopes?.length ? `<div class="panel help"><h2>${m("unavailable")}</h2><p class="small">${m("unavailableHelp", { scopes: opts.unavailableScopes.map((scope) => scopeTitle(scope, locale)).join(", ") })}</p></div>` : ""}
|
|
@@ -53735,11 +54283,11 @@ function renderConfirmPage(opts) {
|
|
|
53735
54283
|
locale
|
|
53736
54284
|
);
|
|
53737
54285
|
}
|
|
53738
|
-
function renderErrorPage(
|
|
53739
|
-
const
|
|
54286
|
+
function renderErrorPage(titleKey, detailKey, locale = "en") {
|
|
54287
|
+
const title = message(titleKey, locale);
|
|
53740
54288
|
return page(
|
|
53741
|
-
|
|
53742
|
-
`<h1>${escapeHtml(
|
|
54289
|
+
title,
|
|
54290
|
+
`<h1>${escapeHtml(title)}</h1><div class="error" role="alert">${escapeHtml(message(detailKey, locale))}</div><p>${escapeHtml(message("restart", locale))}</p>`,
|
|
53743
54291
|
void 0,
|
|
53744
54292
|
locale
|
|
53745
54293
|
);
|
|
@@ -56384,7 +56932,7 @@ var init_oauth = __esm({
|
|
|
56384
56932
|
const sealedRequest = await this.sealAuthRequest({
|
|
56385
56933
|
language,
|
|
56386
56934
|
client_id: client.client_id,
|
|
56387
|
-
client_name: client.client_name || "
|
|
56935
|
+
client_name: client.client_name || message("clientFallback", language),
|
|
56388
56936
|
redirect_uri: params.redirectUri,
|
|
56389
56937
|
code_challenge: params.codeChallenge,
|
|
56390
56938
|
state: params.state,
|
|
@@ -56402,7 +56950,7 @@ var init_oauth = __esm({
|
|
|
56402
56950
|
},
|
|
56403
56951
|
sealedRequest,
|
|
56404
56952
|
client.client_id,
|
|
56405
|
-
client.client_name || "
|
|
56953
|
+
client.client_name || message("clientFallback", language),
|
|
56406
56954
|
requested.length ? requested : [...DEFAULT_REQUESTED_SCOPES],
|
|
56407
56955
|
language,
|
|
56408
56956
|
res
|
|
@@ -56414,7 +56962,7 @@ var init_oauth = __esm({
|
|
|
56414
56962
|
locale: language,
|
|
56415
56963
|
request: sealedRequest,
|
|
56416
56964
|
action: this.consentPath,
|
|
56417
|
-
clientName: client.client_name || "
|
|
56965
|
+
clientName: client.client_name || message("clientFallback", language),
|
|
56418
56966
|
baseUrl: this.options.baseUrl,
|
|
56419
56967
|
backofficeUrl: this.options.backofficeUrl,
|
|
56420
56968
|
scopes: requested.length ? requested : [...DEFAULT_REQUESTED_SCOPES],
|
|
@@ -56429,7 +56977,7 @@ var init_oauth = __esm({
|
|
|
56429
56977
|
locale: language,
|
|
56430
56978
|
request: sealedRequest,
|
|
56431
56979
|
action: this.consentPath,
|
|
56432
|
-
clientName: client.client_name || "
|
|
56980
|
+
clientName: client.client_name || message("clientFallback", language),
|
|
56433
56981
|
baseUrl: this.options.baseUrl,
|
|
56434
56982
|
backofficeUrl: this.options.backofficeUrl,
|
|
56435
56983
|
scopes: requested.length ? requested : [...DEFAULT_REQUESTED_SCOPES]
|
|
@@ -56503,22 +57051,14 @@ var init_oauth = __esm({
|
|
|
56503
57051
|
} catch {
|
|
56504
57052
|
return {
|
|
56505
57053
|
status: 400,
|
|
56506
|
-
html: renderErrorPage(
|
|
56507
|
-
"This sign-in link expired",
|
|
56508
|
-
"Start the connection again from the client that sent you here. A consent link is valid for ten minutes.",
|
|
56509
|
-
fallbackLocale
|
|
56510
|
-
)
|
|
57054
|
+
html: renderErrorPage("expired", "expiredHelp", fallbackLocale)
|
|
56511
57055
|
};
|
|
56512
57056
|
}
|
|
56513
57057
|
const locale = request.language ?? fallbackLocale;
|
|
56514
57058
|
if (request.stage !== "request" && request.stage !== "confirm") {
|
|
56515
57059
|
return {
|
|
56516
57060
|
status: 400,
|
|
56517
|
-
html: renderErrorPage(
|
|
56518
|
-
"Invalid request",
|
|
56519
|
-
"That confirmation is incomplete. Start again.",
|
|
56520
|
-
locale
|
|
56521
|
-
)
|
|
57061
|
+
html: renderErrorPage("invalid", "invalidHelp", locale)
|
|
56522
57062
|
};
|
|
56523
57063
|
}
|
|
56524
57064
|
const redirect = new URL(request.redirect_uri);
|
|
@@ -56539,7 +57079,7 @@ var init_oauth = __esm({
|
|
|
56539
57079
|
locale,
|
|
56540
57080
|
request: await this.sealAuthRequest({ ...original, scopes }),
|
|
56541
57081
|
action: this.consentPath,
|
|
56542
|
-
clientName: request.client_name || "
|
|
57082
|
+
clientName: request.client_name || message("clientFallback", locale),
|
|
56543
57083
|
baseUrl: this.options.baseUrl,
|
|
56544
57084
|
backofficeUrl: this.options.backofficeUrl,
|
|
56545
57085
|
scopes
|
|
@@ -56549,11 +57089,7 @@ var init_oauth = __esm({
|
|
|
56549
57089
|
if (form.action !== "confirm" || !request.pat || !request.identity) {
|
|
56550
57090
|
return {
|
|
56551
57091
|
status: 400,
|
|
56552
|
-
html: renderErrorPage(
|
|
56553
|
-
"Invalid request",
|
|
56554
|
-
"That confirmation is incomplete. Start again.",
|
|
56555
|
-
locale
|
|
56556
|
-
)
|
|
57092
|
+
html: renderErrorPage("invalid", "invalidHelp", locale)
|
|
56557
57093
|
};
|
|
56558
57094
|
}
|
|
56559
57095
|
return {
|
|
@@ -56572,7 +57108,7 @@ var init_oauth = __esm({
|
|
|
56572
57108
|
locale,
|
|
56573
57109
|
request: rawRequest,
|
|
56574
57110
|
action: this.consentPath,
|
|
56575
|
-
clientName: request.client_name || "
|
|
57111
|
+
clientName: request.client_name || message("clientFallback", locale),
|
|
56576
57112
|
baseUrl: this.options.baseUrl,
|
|
56577
57113
|
backofficeUrl: this.options.backofficeUrl,
|
|
56578
57114
|
scopes: request.scopes,
|
|
@@ -56781,7 +57317,7 @@ function createRemoteApp(config2, options = {}) {
|
|
|
56781
57317
|
const code = typeof req.query.code === "string" ? req.query.code : "";
|
|
56782
57318
|
res.redirect(303, await provider.finishConnection(id, code, req.get("Cookie") ?? "", res));
|
|
56783
57319
|
} catch {
|
|
56784
|
-
res.status(400).type("html").send(renderErrorPage("
|
|
57320
|
+
res.status(400).type("html").send(renderErrorPage("failed", "failedHelp", consentLocale(req.get("Accept-Language"))));
|
|
56785
57321
|
}
|
|
56786
57322
|
});
|
|
56787
57323
|
app.post("/consent", import_express7.default.urlencoded({ extended: false, limit: "64kb" }), async (req, res) => {
|
|
@@ -56794,7 +57330,7 @@ function createRemoteApp(config2, options = {}) {
|
|
|
56794
57330
|
res.status(outcome.status).set("Content-Type", "text/html; charset=utf-8").send(outcome.html);
|
|
56795
57331
|
} catch {
|
|
56796
57332
|
res.status(500).set("Content-Type", "text/html; charset=utf-8").send(
|
|
56797
|
-
renderErrorPage("
|
|
57333
|
+
renderErrorPage("unavailableTitle", "unavailableDetail", consentLocale(req.get("Accept-Language")))
|
|
56798
57334
|
);
|
|
56799
57335
|
}
|
|
56800
57336
|
});
|
|
@@ -57049,6 +57585,144 @@ function loadRemoteConfig(env = process.env) {
|
|
|
57049
57585
|
|
|
57050
57586
|
// src/index.ts
|
|
57051
57587
|
init_server3();
|
|
57588
|
+
|
|
57589
|
+
// src/cli.ts
|
|
57590
|
+
init_version();
|
|
57591
|
+
import { cpSync, existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
57592
|
+
import { homedir } from "node:os";
|
|
57593
|
+
import { join } from "node:path";
|
|
57594
|
+
import { fileURLToPath } from "node:url";
|
|
57595
|
+
init_client();
|
|
57596
|
+
init_manifest();
|
|
57597
|
+
var HELP = `appilot-mcp ${SERVER_VERSION}
|
|
57598
|
+
|
|
57599
|
+
The Appilot MCP server: read, validate, fix and provision an Appilot app.
|
|
57600
|
+
|
|
57601
|
+
USAGE
|
|
57602
|
+
appilot-mcp Start the MCP server on stdio (what a client launches)
|
|
57603
|
+
appilot-mcp --http Start the remote HTTP service instead
|
|
57604
|
+
appilot-mcp plan <file> Preview an appilot.app-manifest. Writes nothing
|
|
57605
|
+
appilot-mcp apply <file> Plan, then apply the same manifest
|
|
57606
|
+
appilot-mcp install-skill Copy the app-configurator skill to an agent client
|
|
57607
|
+
appilot-mcp --help | --version
|
|
57608
|
+
|
|
57609
|
+
OPTIONS
|
|
57610
|
+
apply --mode merge|replace How the config half is imported. Default merge
|
|
57611
|
+
--allow-unhealthy Import a bundle the health gate would refuse
|
|
57612
|
+
install-skill --claude | --codex | --cursor | --gemini | --dir <path>
|
|
57613
|
+
|
|
57614
|
+
ENVIRONMENT
|
|
57615
|
+
APPILOT_BASE_URL Your instance, e.g. https://api.appilot.space or http://localhost:6001
|
|
57616
|
+
APPILOT_PAT A scoped service token from the Backoffice, under Service tokens
|
|
57617
|
+
APPILOT_APP_ID Optional default app id
|
|
57618
|
+
|
|
57619
|
+
Docs: https://docs.appilot.space/docs/developers/configure-with-ai/overview`;
|
|
57620
|
+
var SKILL_TARGETS = {
|
|
57621
|
+
claude: join(".claude", "skills"),
|
|
57622
|
+
codex: join(".codex", "skills"),
|
|
57623
|
+
cursor: join(".cursor", "skills"),
|
|
57624
|
+
gemini: join(".gemini", "config", "skills")
|
|
57625
|
+
};
|
|
57626
|
+
function bundledSkillDir(from = fileURLToPath(import.meta.url)) {
|
|
57627
|
+
const candidates = [
|
|
57628
|
+
join(from, "..", "..", "skills", "app-configurator"),
|
|
57629
|
+
join(from, "..", "..", "..", "skills", "app-configurator")
|
|
57630
|
+
];
|
|
57631
|
+
return candidates.find((candidate) => existsSync(join(candidate, "SKILL.md"))) ?? null;
|
|
57632
|
+
}
|
|
57633
|
+
function installSkill(args) {
|
|
57634
|
+
const source = bundledSkillDir();
|
|
57635
|
+
if (!source) {
|
|
57636
|
+
process.stderr.write(
|
|
57637
|
+
"[appilot-mcp] the bundled skill was not found next to this install. Reinstall the package (npm i -g appilot-mcp), or copy skills/app-configurator from the repository by hand.\n"
|
|
57638
|
+
);
|
|
57639
|
+
return 1;
|
|
57640
|
+
}
|
|
57641
|
+
const dirFlag = args.indexOf("--dir");
|
|
57642
|
+
let destParent;
|
|
57643
|
+
if (dirFlag !== -1) {
|
|
57644
|
+
const explicit = args[dirFlag + 1];
|
|
57645
|
+
if (!explicit) {
|
|
57646
|
+
process.stderr.write("[appilot-mcp] --dir needs a path.\n");
|
|
57647
|
+
return 2;
|
|
57648
|
+
}
|
|
57649
|
+
destParent = explicit;
|
|
57650
|
+
} else {
|
|
57651
|
+
const client = Object.keys(SKILL_TARGETS).find((name) => args.includes(`--${name}`));
|
|
57652
|
+
if (!client) {
|
|
57653
|
+
process.stderr.write(
|
|
57654
|
+
`[appilot-mcp] name the client: ${Object.keys(SKILL_TARGETS).map((c) => `--${c}`).join(", ")}, or --dir <path>.
|
|
57655
|
+
`
|
|
57656
|
+
);
|
|
57657
|
+
return 2;
|
|
57658
|
+
}
|
|
57659
|
+
destParent = join(homedir(), SKILL_TARGETS[client]);
|
|
57660
|
+
}
|
|
57661
|
+
const dest = join(destParent, "app-configurator");
|
|
57662
|
+
mkdirSync(destParent, { recursive: true });
|
|
57663
|
+
cpSync(source, dest, { recursive: true });
|
|
57664
|
+
process.stdout.write(`Installed the app-configurator skill to ${dest}
|
|
57665
|
+
Start a new session so the client loads it.
|
|
57666
|
+
`);
|
|
57667
|
+
return 0;
|
|
57668
|
+
}
|
|
57669
|
+
function readManifestFile(path) {
|
|
57670
|
+
if (!path) throw new Error("Name the manifest file: appilot-mcp plan app.appilot.json");
|
|
57671
|
+
if (!existsSync(path)) throw new Error(`No such file: ${path}`);
|
|
57672
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
57673
|
+
}
|
|
57674
|
+
async function runPlan(args) {
|
|
57675
|
+
const conn = loadConnection();
|
|
57676
|
+
const client = new AppilotClient(conn);
|
|
57677
|
+
const manifest = parseManifest(readManifestFile(args[0]));
|
|
57678
|
+
const plan = await planManifest(client, manifest, (id) => {
|
|
57679
|
+
const resolved = id ?? conn.defaultAppId;
|
|
57680
|
+
return resolved != null && Number.isFinite(resolved) ? resolved : null;
|
|
57681
|
+
});
|
|
57682
|
+
process.stdout.write(JSON.stringify(plan, null, 2) + "\n");
|
|
57683
|
+
return 0;
|
|
57684
|
+
}
|
|
57685
|
+
async function runApply(args) {
|
|
57686
|
+
const conn = loadConnection();
|
|
57687
|
+
const client = new AppilotClient(conn);
|
|
57688
|
+
const manifest = parseManifest(readManifestFile(args[0]));
|
|
57689
|
+
const modeFlag = args.indexOf("--mode");
|
|
57690
|
+
const mode = modeFlag !== -1 ? args[modeFlag + 1] : void 0;
|
|
57691
|
+
if (mode !== void 0 && mode !== "merge" && mode !== "replace") {
|
|
57692
|
+
throw new Error(`--mode is merge or replace, not ${mode}.`);
|
|
57693
|
+
}
|
|
57694
|
+
const plan = await planManifest(client, manifest, (id) => {
|
|
57695
|
+
const resolved = id ?? conn.defaultAppId;
|
|
57696
|
+
return resolved != null && Number.isFinite(resolved) ? resolved : null;
|
|
57697
|
+
});
|
|
57698
|
+
process.stderr.write(`[appilot-mcp] planned ${plan.planToken.slice(0, 12)}\u2026
|
|
57699
|
+
`);
|
|
57700
|
+
const result = await applyManifest(client, manifest, {
|
|
57701
|
+
planToken: plan.planToken,
|
|
57702
|
+
mode,
|
|
57703
|
+
expectedCurrentHash: plan.config?.expectedCurrentHash,
|
|
57704
|
+
allowUnhealthy: args.includes("--allow-unhealthy")
|
|
57705
|
+
});
|
|
57706
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
57707
|
+
return 0;
|
|
57708
|
+
}
|
|
57709
|
+
async function runCommand(argv) {
|
|
57710
|
+
const [command, ...rest] = argv;
|
|
57711
|
+
if (command === "--help" || command === "-h" || command === "help") {
|
|
57712
|
+
process.stdout.write(HELP + "\n");
|
|
57713
|
+
return 0;
|
|
57714
|
+
}
|
|
57715
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
57716
|
+
process.stdout.write(SERVER_VERSION + "\n");
|
|
57717
|
+
return 0;
|
|
57718
|
+
}
|
|
57719
|
+
if (command === "install-skill") return installSkill(rest);
|
|
57720
|
+
if (command === "plan") return runPlan(rest);
|
|
57721
|
+
if (command === "apply") return runApply(rest);
|
|
57722
|
+
return null;
|
|
57723
|
+
}
|
|
57724
|
+
|
|
57725
|
+
// src/index.ts
|
|
57052
57726
|
async function runStdio() {
|
|
57053
57727
|
const conn = loadConnection();
|
|
57054
57728
|
const server = createAppilotServer(conn);
|
|
@@ -57061,6 +57735,10 @@ async function runHttp() {
|
|
|
57061
57735
|
await startRemote2(loadRemoteConfig());
|
|
57062
57736
|
}
|
|
57063
57737
|
async function main() {
|
|
57738
|
+
const handled = await runCommand(process.argv.slice(2));
|
|
57739
|
+
if (handled !== null) {
|
|
57740
|
+
process.exit(handled);
|
|
57741
|
+
}
|
|
57064
57742
|
if (resolveTransport() === "http") {
|
|
57065
57743
|
await runHttp();
|
|
57066
57744
|
return;
|