@waniwani/kit 0.1.0 → 0.1.3
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 +31 -22
- package/cli/codegen.mjs +12 -32
- package/cli/framework.mjs +0 -1
- package/cli/index.mjs +24 -190
- package/cli/init.mjs +582 -0
- package/cli/log.mjs +0 -5
- package/cli/scan.mjs +2 -17
- package/cli/validate.mjs +2 -2
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +2 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -50
- package/dist/server.js.map +1 -1
- package/package.json +2 -6
- package/src/index.ts +0 -10
- package/src/server.ts +3 -62
- package/cli/account.mjs +0 -264
- package/cli/tunnel.mjs +0 -140
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* widgets/<name>/widget.ts export default defineWidget({ ... })
|
|
13
13
|
* widgets/<name>/ui.tsx export default function Component() { ... }
|
|
14
14
|
* flows/<name>.ts export default createFlow({ ... }).compile()
|
|
15
|
-
* docs/<slug>.md searchable knowledge
|
|
16
15
|
*/
|
|
17
16
|
export function defineApp(config) {
|
|
18
17
|
return config;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA0CH,MAAM,UAAU,SAAS,CAAC,MAAiB;IAC1C,OAAO,MAAM,CAAC;AACf,CAAC;AAwBD,MAAM,UAAU,UAAU,CACzB,GAAyB;IAEzB,OAAO,GAAG,CAAC;AACZ,CAAC;AAuCD,MAAM,UAAU,YAAY,CAAkB,GAAwB;IACrE,OAAO,GAAG,CAAC;AACZ,CAAC"}
|
package/dist/server.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* hands them to `registerApp()`. Nothing else.
|
|
12
12
|
*/
|
|
13
13
|
import { McpServer } from "skybridge/server";
|
|
14
|
-
import type {
|
|
14
|
+
import type { Shape, ToolHints, WidgetCsp } from "./index.js";
|
|
15
15
|
/**
|
|
16
16
|
* The manifest holds definitions with unrelated schemas side by side, so the
|
|
17
17
|
* handler signatures are widened here. `never` in the parameter position
|
|
@@ -50,7 +50,6 @@ export type Manifest = {
|
|
|
50
50
|
def: AnyWidgetDefinition;
|
|
51
51
|
}>;
|
|
52
52
|
flows: CompiledFlow[];
|
|
53
|
-
docs: DocEntry[];
|
|
54
53
|
/**
|
|
55
54
|
* Origins the template's Tailwind entry loads from, read off it at build
|
|
56
55
|
* time. Every view imports that stylesheet, so every widget needs them.
|
|
@@ -58,8 +57,7 @@ export type Manifest = {
|
|
|
58
57
|
styleDomains?: string[];
|
|
59
58
|
};
|
|
60
59
|
/**
|
|
61
|
-
* Register an app's tools, widgets
|
|
62
|
-
* built.
|
|
60
|
+
* Register an app's tools, widgets and flows onto a server the template built.
|
|
63
61
|
*
|
|
64
62
|
* The template owns construction, its own tools, `withWaniwani`, and `run()`.
|
|
65
63
|
* This adds to that server rather than replacing it, so a tool the template
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAiB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAiB,MAAM,kBAAkB,CAAC;AAC5D,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE9D;;;;GAIG;AACH,KAAK,iBAAiB,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;CAC/B,CAAC;AAEF,KAAK,mBAAmB,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,CAAC;IAClC,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;CACjC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,MAAM,YAAY,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IAEb,MAAM,EAAE,GAAG,CAAC;IAEZ,OAAO,EAAE,GAAG,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACtB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,iBAAiB,CAAA;KAAE,CAAC,CAAC;IACvD,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,mBAAmB,CAAA;KAAE,CAAC,CAAC;IAC3D,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAqEF;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,CA0F3F"}
|
package/dist/server.js
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
* hands them to `registerApp()`. Nothing else.
|
|
12
12
|
*/
|
|
13
13
|
import { McpServer } from "skybridge/server";
|
|
14
|
-
import { z } from "zod";
|
|
15
14
|
/**
|
|
16
15
|
* The origins a widget may load assets from: its own, plus the ones its
|
|
17
16
|
* stylesheet needs.
|
|
@@ -74,52 +73,8 @@ function widgetError(name, error) {
|
|
|
74
73
|
],
|
|
75
74
|
};
|
|
76
75
|
}
|
|
77
|
-
function registerDocsTool(server, docs) {
|
|
78
|
-
const corpus = docs.map((doc) => ({
|
|
79
|
-
...doc,
|
|
80
|
-
haystack: `${doc.title}\n${doc.body}`.toLowerCase(),
|
|
81
|
-
}));
|
|
82
|
-
server.registerTool({
|
|
83
|
-
name: "search_docs",
|
|
84
|
-
title: "Search the documentation",
|
|
85
|
-
description: "Search this product's documentation and answer general questions — pricing, eligibility, policies, how things work. Always search before answering, and answer only from what comes back. Never invent facts that are not in the results.",
|
|
86
|
-
inputSchema: { question: z.string().describe("The user's question, in their own words.") },
|
|
87
|
-
outputSchema: {
|
|
88
|
-
results: z.array(z.object({ slug: z.string(), title: z.string(), body: z.string() })),
|
|
89
|
-
},
|
|
90
|
-
annotations: annotations("Search the documentation", undefined, { readOnly: true }),
|
|
91
|
-
}, async ({ question }) => {
|
|
92
|
-
const terms = question
|
|
93
|
-
.toLowerCase()
|
|
94
|
-
.split(/[^a-z0-9]+/)
|
|
95
|
-
.filter((term) => term.length > 2);
|
|
96
|
-
const results = corpus
|
|
97
|
-
.map((doc) => ({
|
|
98
|
-
doc,
|
|
99
|
-
score: terms.reduce((sum, term) => sum + (doc.haystack.includes(term) ? 1 : 0), 0),
|
|
100
|
-
}))
|
|
101
|
-
.filter(({ score }) => score > 0)
|
|
102
|
-
.sort((a, b) => b.score - a.score)
|
|
103
|
-
.slice(0, 3)
|
|
104
|
-
.map(({ doc }) => ({ slug: doc.slug, title: doc.title, body: doc.body }));
|
|
105
|
-
if (results.length === 0) {
|
|
106
|
-
const text = "Nothing in the documentation covers that question.";
|
|
107
|
-
return { structuredContent: { results: [] }, content: [{ type: "text", text }] };
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
structuredContent: { results },
|
|
111
|
-
content: [
|
|
112
|
-
{
|
|
113
|
-
type: "text",
|
|
114
|
-
text: results.map((r) => `## ${r.title}\n${r.body}`).join("\n\n---\n\n"),
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
};
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
76
|
/**
|
|
121
|
-
* Register an app's tools, widgets
|
|
122
|
-
* built.
|
|
77
|
+
* Register an app's tools, widgets and flows onto a server the template built.
|
|
123
78
|
*
|
|
124
79
|
* The template owns construction, its own tools, `withWaniwani`, and `run()`.
|
|
125
80
|
* This adds to that server rather than replacing it, so a tool the template
|
|
@@ -127,7 +82,7 @@ function registerDocsTool(server, docs) {
|
|
|
127
82
|
* app's own tools sit alongside it.
|
|
128
83
|
*/
|
|
129
84
|
export async function registerApp(server, manifest) {
|
|
130
|
-
const { tools, widgets, flows,
|
|
85
|
+
const { tools, widgets, flows, styleDomains = [] } = manifest;
|
|
131
86
|
// Widgets: one `data` schema drives the input schema, the structured output,
|
|
132
87
|
// and the type the component receives.
|
|
133
88
|
//
|
|
@@ -201,9 +156,6 @@ export async function registerApp(server, manifest) {
|
|
|
201
156
|
for (const flow of flows) {
|
|
202
157
|
server.registerTool({ ...flow.config, name: flow.name }, flow.handler);
|
|
203
158
|
}
|
|
204
|
-
if (docs.length > 0) {
|
|
205
|
-
registerDocsTool(server, docs);
|
|
206
|
-
}
|
|
207
159
|
// `withWaniwani` is deliberately not called here. It wraps every registered
|
|
208
160
|
// handler in place, so it has to run after the last registration — which is
|
|
209
161
|
// the template's, not this function's. `src/server.ts` calls it.
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAiB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAiB,MAAM,kBAAkB,CAAC;AA+C5D;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,GAA0B,EAAE,YAAsB;IAC1E,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,eAAe,IAAI,EAAE,CAAC,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;IAChF,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/C,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAAC,KAAa,EAAE,KAA4B,EAAE,QAAmB;IACpF,MAAM,MAAM,GAAG,EAAE,GAAG,QAAQ,EAAE,GAAG,KAAK,EAAE,CAAC;IACzC,OAAO;QACN,KAAK;QACL,YAAY,EAAE,MAAM,CAAC,QAAQ,IAAI,KAAK;QACtC,eAAe,EAAE,MAAM,CAAC,WAAW,IAAI,KAAK;QAC5C,aAAa,EAAE,MAAM,CAAC,SAAS,IAAI,KAAK;QACxC,cAAc,EAAE,MAAM,CAAC,UAAU,IAAI,KAAK;KAC1C,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,SAAS,UAAU,CAAC,KAAc;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAC9D,CAAC;IACD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,SAAS,IAAI,KAAK,EAAE,CAAC;QAC9D,OAAO,KAA2D,CAAC;IACpE,CAAC;IACD,MAAM,iBAAiB,GAAG,CAAC,KAAK,IAAI,EAAE,CAA4B,CAAC;IACnE,OAAO;QACN,iBAAiB;QACjB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;KACtF,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,IAAY;IACtC,OAAO,OAAO,IAAI,yMAAyM,CAAC;AAC7N,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,KAAc;IAChD,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,CAAC,KAAK,CAAC,sBAAsB,IAAI,mBAAmB,EAAE,KAAK,CAAC,CAAC;IACpE,OAAO;QACN,OAAO,EAAE,IAAa;QACtB,OAAO,EAAE;YACR;gBACC,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,OAAO,IAAI,oCAAoC,OAAO,oGAAoG;aAChK;SACD;KACD,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAiB,EAAE,QAAkB;IACtE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,GAAG,EAAE,EAAE,GAAG,QAAQ,CAAC;IAE9D,6EAA6E;IAC7E,uCAAuC;IACvC,EAAE;IACF,4EAA4E;IAC5E,0EAA0E;IAC1E,wEAAwE;IACxE,mEAAmE;IACnE,KAAK,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,OAAO,EAAE,CAAC;QACrC,MAAM,CAAC,YAAY,CAClB;YACC,IAAI;YACJ,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,WAAW,EAAE,GAAG,CAAC,WAAW;YAC5B,WAAW,EAAE,GAAG,CAAC,IAAI;YACrB,YAAY,EAAE,GAAG,CAAC,IAAI;YACtB,WAAW,EAAE,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YAClE,IAAI,EAAE;gBACL,SAAS,EAAE,IAAgB;gBAC3B,WAAW,EAAE,GAAG,CAAC,WAAW;gBAC5B,GAAG,EAAE;oBACJ,GAAG,GAAG,CAAC,GAAG;oBACV,eAAe,EAAE,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,YAAY,CAAC;iBACvD;aACD;SACD,EACD,KAAK,EAAE,KAAK,EAAE,EAAE;YACf,IAAI,CAAC;gBACJ,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAc,CAAC,CAAC,CAAC,CAAC,KAAK,CAG9D,CAAC;gBACF,OAAO;oBACN,iBAAiB,EAAE,IAAI;oBACvB,OAAO,EAAE;wBACR;4BACC,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,IAAa,CAAC,IAAI,iBAAiB,CAAC,IAAI,CAAC;yBAC7D;qBACD;iBACD,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,OAAO,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACjC,CAAC;QACF,CAAC,CACD,CAAC;IACH,CAAC;IAED,KAAK,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,KAAK,EAAE,CAAC;QACnC,MAAM,CAAC,YAAY,CAClB;YACC,IAAI;YACJ,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,WAAW,EAAE,GAAG,CAAC,WAAW;YAC5B,WAAW,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE;YAC5B,YAAY,EAAE,GAAG,CAAC,MAAM;YACxB,WAAW,EAAE,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;SACnE,EACD,KAAK,EAAE,KAAK,EAAE,EAAE;YACf,IAAI,CAAC;gBACJ,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,KAAc,CAAC,CAAC,CAAC;YAClD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACvE,OAAO,CAAC,KAAK,CAAC,oBAAoB,IAAI,WAAW,EAAE,KAAK,CAAC,CAAC;gBAC1D,OAAO;oBACN,OAAO,EAAE,IAAa;oBACtB,OAAO,EAAE;wBACR;4BACC,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,OAAO,IAAI,iBAAiB,OAAO,+EAA+E;yBACxH;qBACD;iBACD,CAAC;YACH,CAAC;QACF,CAAC,CACD,CAAC;IACH,CAAC;IAED,+EAA+E;IAC/E,6EAA6E;IAC7E,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACxE,CAAC;IAED,4EAA4E;IAC5E,4EAA4E;IAC5E,iEAAiE;IACjE,OAAO,MAAM,CAAC;AACf,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waniwani/kit",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Build an MCP app as a folder: tools, widgets
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Build an MCP app as a folder: tools, widgets and flows, with one CLI and one shared server runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"waniwani": "cli/index.mjs"
|
|
@@ -56,10 +56,6 @@
|
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@skybridge/devtools": "^1.2.7"
|
|
58
58
|
},
|
|
59
|
-
"//optionalDependencies": "cloudflared is what `waniwani tunnel` runs, and its install step fetches a platform binary. Every other command works without it, so a CI image or a container build installing with --omit=optional skips the download and still gets a working CLI.",
|
|
60
|
-
"optionalDependencies": {
|
|
61
|
-
"cloudflared": "^0.7.1"
|
|
62
|
-
},
|
|
63
59
|
"peerDependencies": {
|
|
64
60
|
"react": ">=19",
|
|
65
61
|
"react-dom": ">=19",
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* widgets/<name>/widget.ts export default defineWidget({ ... })
|
|
13
13
|
* widgets/<name>/ui.tsx export default function Component() { ... }
|
|
14
14
|
* flows/<name>.ts export default createFlow({ ... }).compile()
|
|
15
|
-
* docs/<slug>.md searchable knowledge
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
17
|
import type { z } from "zod";
|
|
@@ -127,12 +126,3 @@ export type WidgetDefinition<S extends Shape = Shape> = {
|
|
|
127
126
|
export function defineWidget<S extends Shape>(def: WidgetDefinition<S>): WidgetDefinition<S> {
|
|
128
127
|
return def;
|
|
129
128
|
}
|
|
130
|
-
|
|
131
|
-
// ----------------------------------------------------------------------- docs
|
|
132
|
-
|
|
133
|
-
/** A `docs/<slug>.md` file, parsed at build time and inlined into the bundle. */
|
|
134
|
-
export type DocEntry = {
|
|
135
|
-
slug: string;
|
|
136
|
-
title: string;
|
|
137
|
-
body: string;
|
|
138
|
-
};
|
package/src/server.ts
CHANGED
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { McpServer, type ViewName } from "skybridge/server";
|
|
15
|
-
import {
|
|
16
|
-
import type { DocEntry, Shape, ToolHints, WidgetCsp } from "./index.js";
|
|
15
|
+
import type { Shape, ToolHints, WidgetCsp } from "./index.js";
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* The manifest holds definitions with unrelated schemas side by side, so the
|
|
@@ -52,7 +51,6 @@ export type Manifest = {
|
|
|
52
51
|
tools: Array<{ name: string; def: AnyToolDefinition }>;
|
|
53
52
|
widgets: Array<{ name: string; def: AnyWidgetDefinition }>;
|
|
54
53
|
flows: CompiledFlow[];
|
|
55
|
-
docs: DocEntry[];
|
|
56
54
|
/**
|
|
57
55
|
* Origins the template's Tailwind entry loads from, read off it at build
|
|
58
56
|
* time. Every view imports that stylesheet, so every widget needs them.
|
|
@@ -127,61 +125,8 @@ function widgetError(name: string, error: unknown) {
|
|
|
127
125
|
};
|
|
128
126
|
}
|
|
129
127
|
|
|
130
|
-
function registerDocsTool(server: McpServer, docs: DocEntry[]) {
|
|
131
|
-
const corpus = docs.map((doc) => ({
|
|
132
|
-
...doc,
|
|
133
|
-
haystack: `${doc.title}\n${doc.body}`.toLowerCase(),
|
|
134
|
-
}));
|
|
135
|
-
|
|
136
|
-
server.registerTool(
|
|
137
|
-
{
|
|
138
|
-
name: "search_docs",
|
|
139
|
-
title: "Search the documentation",
|
|
140
|
-
description:
|
|
141
|
-
"Search this product's documentation and answer general questions — pricing, eligibility, policies, how things work. Always search before answering, and answer only from what comes back. Never invent facts that are not in the results.",
|
|
142
|
-
inputSchema: { question: z.string().describe("The user's question, in their own words.") },
|
|
143
|
-
outputSchema: {
|
|
144
|
-
results: z.array(z.object({ slug: z.string(), title: z.string(), body: z.string() })),
|
|
145
|
-
},
|
|
146
|
-
annotations: annotations("Search the documentation", undefined, { readOnly: true }),
|
|
147
|
-
},
|
|
148
|
-
async ({ question }) => {
|
|
149
|
-
const terms = question
|
|
150
|
-
.toLowerCase()
|
|
151
|
-
.split(/[^a-z0-9]+/)
|
|
152
|
-
.filter((term) => term.length > 2);
|
|
153
|
-
|
|
154
|
-
const results = corpus
|
|
155
|
-
.map((doc) => ({
|
|
156
|
-
doc,
|
|
157
|
-
score: terms.reduce((sum, term) => sum + (doc.haystack.includes(term) ? 1 : 0), 0),
|
|
158
|
-
}))
|
|
159
|
-
.filter(({ score }) => score > 0)
|
|
160
|
-
.sort((a, b) => b.score - a.score)
|
|
161
|
-
.slice(0, 3)
|
|
162
|
-
.map(({ doc }) => ({ slug: doc.slug, title: doc.title, body: doc.body }));
|
|
163
|
-
|
|
164
|
-
if (results.length === 0) {
|
|
165
|
-
const text = "Nothing in the documentation covers that question.";
|
|
166
|
-
return { structuredContent: { results: [] }, content: [{ type: "text" as const, text }] };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
structuredContent: { results },
|
|
171
|
-
content: [
|
|
172
|
-
{
|
|
173
|
-
type: "text" as const,
|
|
174
|
-
text: results.map((r) => `## ${r.title}\n${r.body}`).join("\n\n---\n\n"),
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
};
|
|
178
|
-
},
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
128
|
/**
|
|
183
|
-
* Register an app's tools, widgets
|
|
184
|
-
* built.
|
|
129
|
+
* Register an app's tools, widgets and flows onto a server the template built.
|
|
185
130
|
*
|
|
186
131
|
* The template owns construction, its own tools, `withWaniwani`, and `run()`.
|
|
187
132
|
* This adds to that server rather than replacing it, so a tool the template
|
|
@@ -189,7 +134,7 @@ function registerDocsTool(server: McpServer, docs: DocEntry[]) {
|
|
|
189
134
|
* app's own tools sit alongside it.
|
|
190
135
|
*/
|
|
191
136
|
export async function registerApp(server: McpServer, manifest: Manifest): Promise<McpServer> {
|
|
192
|
-
const { tools, widgets, flows,
|
|
137
|
+
const { tools, widgets, flows, styleDomains = [] } = manifest;
|
|
193
138
|
|
|
194
139
|
// Widgets: one `data` schema drives the input schema, the structured output,
|
|
195
140
|
// and the type the component receives.
|
|
@@ -274,10 +219,6 @@ export async function registerApp(server: McpServer, manifest: Manifest): Promis
|
|
|
274
219
|
server.registerTool({ ...flow.config, name: flow.name }, flow.handler);
|
|
275
220
|
}
|
|
276
221
|
|
|
277
|
-
if (docs.length > 0) {
|
|
278
|
-
registerDocsTool(server, docs);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
222
|
// `withWaniwani` is deliberately not called here. It wraps every registered
|
|
282
223
|
// handler in place, so it has to run after the last registration — which is
|
|
283
224
|
// the template's, not this function's. `src/server.ts` calls it.
|
package/cli/account.mjs
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The WaniWani account boundary.
|
|
3
|
-
*
|
|
4
|
-
* `tunnel` is the one command in this CLI that talks to app.waniwani.ai, and it
|
|
5
|
-
* needs two facts: who the developer is, and which agent this repo is bound to.
|
|
6
|
-
* Both already have a home that `@waniwani/cli` writes and `@waniwani/sdk`
|
|
7
|
-
* reads, so this file invents no third place to look:
|
|
8
|
-
*
|
|
9
|
-
* ~/.config/waniwani/settings.json credentials and the instance they were
|
|
10
|
-
* issued for. One login per machine, mode
|
|
11
|
-
* 0600, honours XDG_CONFIG_HOME.
|
|
12
|
-
* ./waniwani.json orgId, projectId, apiUrl, devPort. The
|
|
13
|
-
* file `waniwani connect` writes, and the
|
|
14
|
-
* one the SDK loads at runtime.
|
|
15
|
-
*
|
|
16
|
-
* `.waniwani/` holds neither of them. In the kit that directory is build output:
|
|
17
|
-
* every command regenerates it, the app's .gitignore covers it, and `eject` says
|
|
18
|
-
* to delete it. A refresh token written there lasts until the next `waniwani
|
|
19
|
-
* dev`. This is the same reasoning that moved the login CLI's own credentials
|
|
20
|
-
* out of it.
|
|
21
|
-
*
|
|
22
|
-
* `WANIWANI_API_KEY` is ignored here on purpose. A kit app usually carries that
|
|
23
|
-
* key for tracking, where it is scoped to the project's production environment,
|
|
24
|
-
* while the tunnel and dev-session routes are about the human at the terminal.
|
|
25
|
-
* Auth is therefore the OAuth token, refreshed in place when it has aged out.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { spawn } from "node:child_process";
|
|
29
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
30
|
-
import { homedir } from "node:os";
|
|
31
|
-
import { dirname, join } from "node:path";
|
|
32
|
-
import { fileURLToPath } from "node:url";
|
|
33
|
-
import { dim } from "./log.mjs";
|
|
34
|
-
|
|
35
|
-
const DEFAULT_API_URL = "https://app.waniwani.ai";
|
|
36
|
-
|
|
37
|
-
const CONFIG_HOME = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
38
|
-
const SETTINGS_FILE = join(CONFIG_HOME, "waniwani", "settings.json");
|
|
39
|
-
const DIR_MODE = 0o700;
|
|
40
|
-
const FILE_MODE = 0o600;
|
|
41
|
-
|
|
42
|
-
/** A token this close to expiry is treated as expired, so a long run doesn't 401 mid-flight. */
|
|
43
|
-
const EXPIRY_SKEW_MS = 5 * 60 * 1000;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The CLI that owns logging in and binding a repo to an agent.
|
|
47
|
-
*
|
|
48
|
-
* Those two flows are an OAuth2 PKCE round trip with a local callback server,
|
|
49
|
-
* and a pair of pickers over the org's agents. Both live in `@waniwani/cli`
|
|
50
|
-
* already, and both write exactly the files above, so this CLI drives it for
|
|
51
|
-
* them instead of carrying a second copy that has to stay in step.
|
|
52
|
-
*/
|
|
53
|
-
const LOGIN_CLI = "@waniwani/cli";
|
|
54
|
-
|
|
55
|
-
function readSettings() {
|
|
56
|
-
try {
|
|
57
|
-
return JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
|
|
58
|
-
} catch {
|
|
59
|
-
return {};
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function writeSettings(settings) {
|
|
64
|
-
mkdirSync(dirname(SETTINGS_FILE), { recursive: true, mode: DIR_MODE });
|
|
65
|
-
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, "\t"));
|
|
66
|
-
chmodSync(SETTINGS_FILE, FILE_MODE);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The repo's `waniwani.json`, or null when it has never been connected.
|
|
71
|
-
*
|
|
72
|
-
* Only the JSON file counts. An app folder also has a `waniwani.config.ts`, and
|
|
73
|
-
* in the kit that is the app itself (`defineApp`), which has nothing to say
|
|
74
|
-
* about org or agent ids.
|
|
75
|
-
*/
|
|
76
|
-
export function readProjectConfig(appRoot) {
|
|
77
|
-
const file = join(appRoot, "waniwani.json");
|
|
78
|
-
if (!existsSync(file)) return null;
|
|
79
|
-
try {
|
|
80
|
-
return JSON.parse(readFileSync(file, "utf-8"));
|
|
81
|
-
} catch (error) {
|
|
82
|
-
throw new Error(`${file} is not valid JSON: ${error.message}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Which instance to talk to, most specific wins. Mirrors the login CLI's order. */
|
|
87
|
-
function resolveApiUrl(project) {
|
|
88
|
-
return process.env.WANIWANI_API_URL || project?.apiUrl || readSettings().apiUrl || DEFAULT_API_URL;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function isExpired(settings) {
|
|
92
|
-
if (!settings.expiresAt) return true;
|
|
93
|
-
return new Date(settings.expiresAt).getTime() - EXPIRY_SKEW_MS < Date.now();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Trade the refresh token for a new access token and persist both.
|
|
98
|
-
*
|
|
99
|
-
* `resource` is RFC 8707 and load-bearing: without it the OAuth server issues an
|
|
100
|
-
* opaque token, which then fails JWKS validation on every API call.
|
|
101
|
-
*/
|
|
102
|
-
async function refreshTokens(apiUrl) {
|
|
103
|
-
const settings = readSettings();
|
|
104
|
-
if (!settings.refreshToken || !settings.clientId) return null;
|
|
105
|
-
|
|
106
|
-
const response = await fetch(`${apiUrl}/api/auth/oauth2/token`, {
|
|
107
|
-
method: "POST",
|
|
108
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
109
|
-
body: new URLSearchParams({
|
|
110
|
-
grant_type: "refresh_token",
|
|
111
|
-
refresh_token: settings.refreshToken,
|
|
112
|
-
client_id: settings.clientId,
|
|
113
|
-
resource: apiUrl,
|
|
114
|
-
}).toString(),
|
|
115
|
-
});
|
|
116
|
-
if (!response.ok) return null;
|
|
117
|
-
|
|
118
|
-
const tokens = await response.json();
|
|
119
|
-
writeSettings({
|
|
120
|
-
...settings,
|
|
121
|
-
accessToken: tokens.access_token,
|
|
122
|
-
refreshToken: tokens.refresh_token,
|
|
123
|
-
expiresAt: new Date(Date.now() + tokens.expires_in * 1000).toISOString(),
|
|
124
|
-
});
|
|
125
|
-
return tokens.access_token;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* The stored token when it is usable against `apiUrl`, else null.
|
|
130
|
-
*
|
|
131
|
-
* A token issued for another instance counts as no token at all: the US and EU
|
|
132
|
-
* deployments have separate identities, so carrying one over produces a 401 that
|
|
133
|
-
* reads like a broken account.
|
|
134
|
-
*/
|
|
135
|
-
async function usableToken(apiUrl) {
|
|
136
|
-
const settings = readSettings();
|
|
137
|
-
if (!settings.accessToken) return null;
|
|
138
|
-
if (settings.apiUrl && settings.apiUrl !== apiUrl) return null;
|
|
139
|
-
if (!isExpired(settings)) return settings.accessToken;
|
|
140
|
-
return refreshTokens(apiUrl);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** The login CLI's entry file when it is installed alongside us, else null. */
|
|
144
|
-
function localLoginCli() {
|
|
145
|
-
try {
|
|
146
|
-
return fileURLToPath(import.meta.resolve(LOGIN_CLI));
|
|
147
|
-
} catch {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Hand the terminal to the login CLI for one subcommand.
|
|
154
|
-
*
|
|
155
|
-
* An installed copy is run through `node` by absolute path. Both packages
|
|
156
|
-
* publish a `waniwani` binary, so going through `node_modules/.bin` would leave
|
|
157
|
-
* it to install order which of the two answers.
|
|
158
|
-
*/
|
|
159
|
-
async function runLoginCli(subcommand) {
|
|
160
|
-
const entry = localLoginCli();
|
|
161
|
-
const [command, args] = entry
|
|
162
|
-
? [process.execPath, [entry, subcommand]]
|
|
163
|
-
: ["npx", ["-y", `${LOGIN_CLI}@latest`, subcommand]];
|
|
164
|
-
|
|
165
|
-
console.log(dim(`[waniwani] running ${LOGIN_CLI} ${subcommand}…`));
|
|
166
|
-
const code = await new Promise((resolve) => {
|
|
167
|
-
const child = spawn(command, args, { stdio: "inherit" });
|
|
168
|
-
child.on("close", (exit) => resolve(exit ?? 1));
|
|
169
|
-
child.on("error", () => resolve(1));
|
|
170
|
-
});
|
|
171
|
-
if (code !== 0) {
|
|
172
|
-
throw new Error(`\`waniwani ${subcommand}\` exited with code ${code}`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Resolve the account this repo runs against, filling in whatever is missing.
|
|
178
|
-
*
|
|
179
|
-
* A machine with no credentials gets the login flow, and a repo with no
|
|
180
|
-
* `waniwani.json` gets the connect flow, both in the terminal the user is
|
|
181
|
-
* already sitting in. When they come back the files are on disk and every later
|
|
182
|
-
* run of this command is silent.
|
|
183
|
-
*/
|
|
184
|
-
export async function connectAccount(appRoot) {
|
|
185
|
-
let project = readProjectConfig(appRoot);
|
|
186
|
-
let apiUrl = resolveApiUrl(project);
|
|
187
|
-
|
|
188
|
-
if (!(await usableToken(apiUrl))) {
|
|
189
|
-
await runLoginCli("login");
|
|
190
|
-
project = readProjectConfig(appRoot);
|
|
191
|
-
apiUrl = resolveApiUrl(project);
|
|
192
|
-
if (!(await usableToken(apiUrl))) {
|
|
193
|
-
throw new Error(`no credentials at ${SETTINGS_FILE} after logging in`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (!project?.projectId) {
|
|
198
|
-
await runLoginCli("connect");
|
|
199
|
-
project = readProjectConfig(appRoot);
|
|
200
|
-
}
|
|
201
|
-
if (!project?.projectId) {
|
|
202
|
-
throw new Error(`no projectId in ${join(appRoot, "waniwani.json")}: run \`waniwani connect\` to bind this repo to an agent`);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
apiUrl,
|
|
207
|
-
projectId: project.projectId,
|
|
208
|
-
devPort: project.devPort,
|
|
209
|
-
playgroundUrl: `${apiUrl}/agents/${project.projectId}/playground?localMode=1`,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* An authenticated client for one instance.
|
|
215
|
-
*
|
|
216
|
-
* Responses come back in the API's `{ success, data, error }` envelope, so the
|
|
217
|
-
* payload is unwrapped here and a failure is raised as an ordinary Error the
|
|
218
|
-
* CLI's top-level handler prints. A 401 buys one refresh and one retry, which
|
|
219
|
-
* covers a token that aged out during a long dev session.
|
|
220
|
-
*/
|
|
221
|
-
export function createClient(apiUrl) {
|
|
222
|
-
const send = async (method, path, body, retry = true) => {
|
|
223
|
-
const token = await usableToken(apiUrl);
|
|
224
|
-
if (!token) {
|
|
225
|
-
throw new Error("not logged in: run `waniwani login`");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const response = await fetch(`${apiUrl}${path}`, {
|
|
229
|
-
method,
|
|
230
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
231
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
if (response.status === 204) return undefined;
|
|
235
|
-
|
|
236
|
-
const text = await response.text();
|
|
237
|
-
let payload;
|
|
238
|
-
try {
|
|
239
|
-
payload = JSON.parse(text);
|
|
240
|
-
} catch {
|
|
241
|
-
throw new Error(`${method} ${path} failed with ${response.status}: ${text.slice(0, 200)}`);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (response.ok && !payload.error) return payload.data;
|
|
245
|
-
|
|
246
|
-
if (response.status === 401 && retry && (await refreshTokens(apiUrl))) {
|
|
247
|
-
return send(method, path, body, false);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const error = payload.error;
|
|
251
|
-
const message =
|
|
252
|
-
(typeof error === "object" && error?.message) ||
|
|
253
|
-
payload.message ||
|
|
254
|
-
(typeof error === "string" ? error : null) ||
|
|
255
|
-
`${method} ${path} failed with ${response.status}`;
|
|
256
|
-
throw new Error(message);
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
return {
|
|
260
|
-
post: (path, body) => send("POST", path, body),
|
|
261
|
-
patch: (path) => send("PATCH", path),
|
|
262
|
-
delete: (path) => send("DELETE", path),
|
|
263
|
-
};
|
|
264
|
-
}
|