multisite-cms-mcp 1.0.13 → 1.0.16
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 +165 -9
- package/dist/index.js +31 -0
- package/dist/lib/api-client.d.ts +65 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +157 -0
- package/dist/lib/credentials.d.ts +52 -0
- package/dist/lib/credentials.d.ts.map +1 -0
- package/dist/lib/credentials.js +196 -0
- package/dist/lib/device-flow.d.ts +14 -0
- package/dist/lib/device-flow.d.ts.map +1 -0
- package/dist/lib/device-flow.js +235 -0
- package/dist/tools/get-conversion-guide.d.ts.map +1 -1
- package/dist/tools/get-conversion-guide.js +55 -2
- package/dist/tools/get-example.d.ts.map +1 -1
- package/dist/tools/get-example.js +36 -14
- package/dist/tools/get-schema.d.ts.map +1 -1
- package/dist/tools/get-schema.js +148 -8
- package/dist/tools/get-tenant-schema.d.ts +12 -0
- package/dist/tools/get-tenant-schema.d.ts.map +1 -0
- package/dist/tools/get-tenant-schema.js +146 -0
- package/dist/tools/list-projects.d.ts +5 -0
- package/dist/tools/list-projects.d.ts.map +1 -0
- package/dist/tools/list-projects.js +101 -0
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getCredentialsPath = getCredentialsPath;
|
|
37
|
+
exports.loadCredentials = loadCredentials;
|
|
38
|
+
exports.saveCredentials = saveCredentials;
|
|
39
|
+
exports.deleteCredentials = deleteCredentials;
|
|
40
|
+
exports.hasCredentials = hasCredentials;
|
|
41
|
+
exports.isTokenExpired = isTokenExpired;
|
|
42
|
+
exports.getApiUrl = getApiUrl;
|
|
43
|
+
exports.refreshAccessToken = refreshAccessToken;
|
|
44
|
+
exports.getValidCredentials = getValidCredentials;
|
|
45
|
+
exports.getAuthToken = getAuthToken;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const os = __importStar(require("os"));
|
|
49
|
+
/**
|
|
50
|
+
* Get the path to the credentials file
|
|
51
|
+
*/
|
|
52
|
+
function getCredentialsPath() {
|
|
53
|
+
const configDir = path.join(os.homedir(), '.fastmode');
|
|
54
|
+
return path.join(configDir, 'credentials.json');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Ensure the config directory exists
|
|
58
|
+
*/
|
|
59
|
+
function ensureConfigDir() {
|
|
60
|
+
const configDir = path.dirname(getCredentialsPath());
|
|
61
|
+
if (!fs.existsSync(configDir)) {
|
|
62
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Load stored credentials from disk
|
|
67
|
+
*/
|
|
68
|
+
function loadCredentials() {
|
|
69
|
+
const credentialsPath = getCredentialsPath();
|
|
70
|
+
try {
|
|
71
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const content = fs.readFileSync(credentialsPath, 'utf-8');
|
|
75
|
+
const credentials = JSON.parse(content);
|
|
76
|
+
// Validate required fields
|
|
77
|
+
if (!credentials.accessToken || !credentials.refreshToken || !credentials.expiresAt || !credentials.email) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return credentials;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Save credentials to disk
|
|
88
|
+
*/
|
|
89
|
+
function saveCredentials(credentials) {
|
|
90
|
+
ensureConfigDir();
|
|
91
|
+
const credentialsPath = getCredentialsPath();
|
|
92
|
+
// Write with restricted permissions (owner read/write only)
|
|
93
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Delete stored credentials
|
|
97
|
+
*/
|
|
98
|
+
function deleteCredentials() {
|
|
99
|
+
const credentialsPath = getCredentialsPath();
|
|
100
|
+
try {
|
|
101
|
+
if (fs.existsSync(credentialsPath)) {
|
|
102
|
+
fs.unlinkSync(credentialsPath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Ignore errors
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if credentials exist
|
|
111
|
+
*/
|
|
112
|
+
function hasCredentials() {
|
|
113
|
+
return loadCredentials() !== null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check if the access token is expired or about to expire
|
|
117
|
+
*/
|
|
118
|
+
function isTokenExpired(credentials, bufferMinutes = 5) {
|
|
119
|
+
const expiresAt = new Date(credentials.expiresAt);
|
|
120
|
+
const bufferMs = bufferMinutes * 60 * 1000;
|
|
121
|
+
return Date.now() >= expiresAt.getTime() - bufferMs;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get the API URL from environment or default
|
|
125
|
+
*/
|
|
126
|
+
function getApiUrl() {
|
|
127
|
+
return process.env.FASTMODE_API_URL || 'https://api.fastmode.ai';
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Refresh the access token using the refresh token
|
|
131
|
+
*/
|
|
132
|
+
async function refreshAccessToken(credentials) {
|
|
133
|
+
const apiUrl = getApiUrl();
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch(`${apiUrl}/api/auth/device/refresh`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
refresh_token: credentials.refreshToken,
|
|
142
|
+
grant_type: 'refresh_token',
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
// Refresh token is invalid or expired - need to re-authenticate
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const data = await response.json();
|
|
150
|
+
if (!data.success || !data.data) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const newCredentials = {
|
|
154
|
+
accessToken: data.data.access_token,
|
|
155
|
+
refreshToken: data.data.refresh_token,
|
|
156
|
+
expiresAt: new Date(Date.now() + data.data.expires_in * 1000).toISOString(),
|
|
157
|
+
email: data.data.email,
|
|
158
|
+
name: data.data.name,
|
|
159
|
+
};
|
|
160
|
+
// Save the new credentials
|
|
161
|
+
saveCredentials(newCredentials);
|
|
162
|
+
return newCredentials;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get valid credentials, refreshing if needed
|
|
170
|
+
* Returns null if no credentials or refresh fails
|
|
171
|
+
*/
|
|
172
|
+
async function getValidCredentials() {
|
|
173
|
+
const credentials = loadCredentials();
|
|
174
|
+
if (!credentials) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
// Check if token is expired or about to expire
|
|
178
|
+
if (isTokenExpired(credentials)) {
|
|
179
|
+
// Try to refresh
|
|
180
|
+
const newCredentials = await refreshAccessToken(credentials);
|
|
181
|
+
if (!newCredentials) {
|
|
182
|
+
// Refresh failed - credentials are invalid
|
|
183
|
+
deleteCredentials();
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return newCredentials;
|
|
187
|
+
}
|
|
188
|
+
return credentials;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get the current auth token (access token) if available and valid
|
|
192
|
+
*/
|
|
193
|
+
async function getAuthToken() {
|
|
194
|
+
const credentials = await getValidCredentials();
|
|
195
|
+
return credentials?.accessToken || null;
|
|
196
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start the device authorization flow
|
|
3
|
+
* Returns a message describing the flow status
|
|
4
|
+
*/
|
|
5
|
+
export declare function startDeviceFlow(): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Check if device flow authentication is needed and perform it if so
|
|
8
|
+
* Returns the authentication result message
|
|
9
|
+
*/
|
|
10
|
+
export declare function ensureAuthenticated(): Promise<{
|
|
11
|
+
authenticated: boolean;
|
|
12
|
+
message: string;
|
|
13
|
+
}>;
|
|
14
|
+
//# sourceMappingURL=device-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/lib/device-flow.ts"],"names":[],"mappings":"AA0DA;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CA+FvD;AA0ED;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAuBhG"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.startDeviceFlow = startDeviceFlow;
|
|
37
|
+
exports.ensureAuthenticated = ensureAuthenticated;
|
|
38
|
+
const child_process_1 = require("child_process");
|
|
39
|
+
const credentials_1 = require("./credentials");
|
|
40
|
+
/**
|
|
41
|
+
* Open a URL in the default browser
|
|
42
|
+
*/
|
|
43
|
+
function openBrowser(url) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
// Determine the command based on platform
|
|
46
|
+
let command;
|
|
47
|
+
switch (process.platform) {
|
|
48
|
+
case 'darwin':
|
|
49
|
+
command = `open "${url}"`;
|
|
50
|
+
break;
|
|
51
|
+
case 'win32':
|
|
52
|
+
command = `start "" "${url}"`;
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
// Linux and others
|
|
56
|
+
command = `xdg-open "${url}"`;
|
|
57
|
+
}
|
|
58
|
+
(0, child_process_1.exec)(command, (error) => {
|
|
59
|
+
if (error) {
|
|
60
|
+
// Don't fail if browser can't open - user can manually navigate
|
|
61
|
+
resolve();
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
resolve();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Start the device authorization flow
|
|
71
|
+
* Returns a message describing the flow status
|
|
72
|
+
*/
|
|
73
|
+
async function startDeviceFlow() {
|
|
74
|
+
const apiUrl = (0, credentials_1.getApiUrl)();
|
|
75
|
+
// Step 1: Request device authorization
|
|
76
|
+
let authResponse;
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(`${apiUrl}/api/auth/device/authorize`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
clientName: 'FastMode MCP',
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
89
|
+
return `# Authentication Error
|
|
90
|
+
|
|
91
|
+
Failed to start device authorization: ${error.error || response.statusText}
|
|
92
|
+
|
|
93
|
+
Please check:
|
|
94
|
+
1. Your network connection
|
|
95
|
+
2. The API URL is correct (${apiUrl})
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
authResponse = data.data;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return `# Network Error
|
|
103
|
+
|
|
104
|
+
Unable to connect to FastMode API.
|
|
105
|
+
|
|
106
|
+
**API URL:** ${apiUrl}
|
|
107
|
+
**Error:** ${error instanceof Error ? error.message : 'Unknown error'}
|
|
108
|
+
|
|
109
|
+
Please check your network connection and try again.
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
// Step 2: Open browser for user authorization
|
|
113
|
+
try {
|
|
114
|
+
await openBrowser(authResponse.verification_uri_complete);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Browser failed to open - user will need to navigate manually
|
|
118
|
+
}
|
|
119
|
+
// Step 3: Poll for authorization
|
|
120
|
+
const pollInterval = (authResponse.interval || 5) * 1000; // seconds to ms
|
|
121
|
+
const expiresAt = Date.now() + authResponse.expires_in * 1000;
|
|
122
|
+
// Log instructions to stderr for user visibility
|
|
123
|
+
console.error(`
|
|
124
|
+
# Device Authorization
|
|
125
|
+
|
|
126
|
+
A browser window should open automatically.
|
|
127
|
+
|
|
128
|
+
If it doesn't, please visit:
|
|
129
|
+
${authResponse.verification_uri}
|
|
130
|
+
|
|
131
|
+
And enter this code: ${authResponse.user_code}
|
|
132
|
+
|
|
133
|
+
Waiting for authorization...
|
|
134
|
+
`);
|
|
135
|
+
// Start polling
|
|
136
|
+
const pollResult = await pollForToken(apiUrl, authResponse.device_code, pollInterval, expiresAt);
|
|
137
|
+
if (pollResult.success && pollResult.credentials) {
|
|
138
|
+
// Save credentials
|
|
139
|
+
(0, credentials_1.saveCredentials)(pollResult.credentials);
|
|
140
|
+
return `# Authentication Successful
|
|
141
|
+
|
|
142
|
+
Logged in as: **${pollResult.credentials.email}**${pollResult.credentials.name ? ` (${pollResult.credentials.name})` : ''}
|
|
143
|
+
|
|
144
|
+
Credentials saved to ~/.fastmode/credentials.json
|
|
145
|
+
|
|
146
|
+
You can now use FastMode MCP tools.
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
return `# Authentication Failed
|
|
151
|
+
|
|
152
|
+
${pollResult.error || 'Authorization timed out or was denied.'}
|
|
153
|
+
|
|
154
|
+
Please try again with \`list_projects\` or \`get_tenant_schema\`.
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Poll the token endpoint until authorization is complete or timeout
|
|
160
|
+
*/
|
|
161
|
+
async function pollForToken(apiUrl, deviceCode, interval, expiresAt) {
|
|
162
|
+
while (Date.now() < expiresAt) {
|
|
163
|
+
// Wait for the polling interval
|
|
164
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
165
|
+
try {
|
|
166
|
+
const response = await fetch(`${apiUrl}/api/auth/device/token`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
'Content-Type': 'application/json',
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
device_code: deviceCode,
|
|
173
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
const data = await response.json();
|
|
177
|
+
if (response.ok && data.success && data.data) {
|
|
178
|
+
// Authorization successful!
|
|
179
|
+
const tokenData = data.data;
|
|
180
|
+
const credentials = {
|
|
181
|
+
accessToken: tokenData.access_token,
|
|
182
|
+
refreshToken: tokenData.refresh_token,
|
|
183
|
+
expiresAt: new Date(Date.now() + tokenData.expires_in * 1000).toISOString(),
|
|
184
|
+
email: tokenData.email,
|
|
185
|
+
name: tokenData.name,
|
|
186
|
+
};
|
|
187
|
+
return { success: true, credentials };
|
|
188
|
+
}
|
|
189
|
+
// Check for specific error codes
|
|
190
|
+
if (data.error === 'authorization_pending') {
|
|
191
|
+
// User hasn't authorized yet - keep polling
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (data.error === 'slow_down') {
|
|
195
|
+
// Server is asking us to slow down
|
|
196
|
+
interval = Math.min(interval * 2, 30000); // Max 30 seconds
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (data.error === 'expired_token') {
|
|
200
|
+
return { success: false, error: 'The authorization request expired. Please try again.' };
|
|
201
|
+
}
|
|
202
|
+
if (data.error === 'access_denied') {
|
|
203
|
+
return { success: false, error: 'Authorization was denied. Please try again.' };
|
|
204
|
+
}
|
|
205
|
+
// Unknown error - keep polling
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Network error - keep trying
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { success: false, error: 'Authorization timed out. Please try again.' };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Check if device flow authentication is needed and perform it if so
|
|
215
|
+
* Returns the authentication result message
|
|
216
|
+
*/
|
|
217
|
+
async function ensureAuthenticated() {
|
|
218
|
+
// Import here to avoid circular dependency
|
|
219
|
+
const { getValidCredentials } = await Promise.resolve().then(() => __importStar(require('./credentials')));
|
|
220
|
+
const credentials = await getValidCredentials();
|
|
221
|
+
if (credentials) {
|
|
222
|
+
return {
|
|
223
|
+
authenticated: true,
|
|
224
|
+
message: `Authenticated as ${credentials.email}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// Need to authenticate
|
|
228
|
+
const result = await startDeviceFlow();
|
|
229
|
+
// Check if authentication was successful
|
|
230
|
+
const newCredentials = await getValidCredentials();
|
|
231
|
+
return {
|
|
232
|
+
authenticated: !!newCredentials,
|
|
233
|
+
message: result,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-conversion-guide.d.ts","sourceRoot":"","sources":["../../src/tools/get-conversion-guide.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"get-conversion-guide.d.ts","sourceRoot":"","sources":["../../src/tools/get-conversion-guide.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AAysB1H;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAkD1E"}
|
|
@@ -422,6 +422,20 @@ Use for rich text content:
|
|
|
422
422
|
{{/unless}}
|
|
423
423
|
\`\`\`
|
|
424
424
|
|
|
425
|
+
## Equality Comparisons
|
|
426
|
+
\`\`\`html
|
|
427
|
+
<!-- Show when equal -->
|
|
428
|
+
{{#if (eq fieldA fieldB)}}...{{/if}}
|
|
429
|
+
|
|
430
|
+
<!-- Show when NOT equal (common for Related Posts) -->
|
|
431
|
+
{{#unless (eq slug ../slug)}}
|
|
432
|
+
<a href="{{url}}">{{name}}</a>
|
|
433
|
+
{{/unless}}
|
|
434
|
+
|
|
435
|
+
<!-- Compare to literal string -->
|
|
436
|
+
{{#eq status "published"}}...{{/eq}}
|
|
437
|
+
\`\`\`
|
|
438
|
+
|
|
425
439
|
## Loop Variables
|
|
426
440
|
Inside {{#each}}:
|
|
427
441
|
- \`{{@first}}\` - true for first item
|
|
@@ -431,12 +445,19 @@ Inside {{#each}}:
|
|
|
431
445
|
## Parent Context (\`../\`)
|
|
432
446
|
Inside loops, access the parent scope:
|
|
433
447
|
\`\`\`html
|
|
448
|
+
<!-- Filter posts by current author -->
|
|
434
449
|
{{#each blogs}}
|
|
435
450
|
{{#if (eq author.name ../name)}}
|
|
436
|
-
<!-- Only show posts by current author -->
|
|
437
451
|
<h3>{{name}}</h3>
|
|
438
452
|
{{/if}}
|
|
439
453
|
{{/each}}
|
|
454
|
+
|
|
455
|
+
<!-- Related Posts (exclude current) -->
|
|
456
|
+
{{#each blogs limit=3}}
|
|
457
|
+
{{#unless (eq slug ../slug)}}
|
|
458
|
+
<a href="{{url}}">{{name}}</a>
|
|
459
|
+
{{/unless}}
|
|
460
|
+
{{/each}}
|
|
440
461
|
\`\`\`
|
|
441
462
|
|
|
442
463
|
- \`../name\` - Parent item's name field
|
|
@@ -481,7 +502,39 @@ Compare two values:
|
|
|
481
502
|
|
|
482
503
|
**Downloads:**
|
|
483
504
|
- {{title}}, {{slug}}, {{description}}
|
|
484
|
-
- {{fileUrl}}, {{category}}, {{order}}
|
|
505
|
+
- {{fileUrl}}, {{category}}, {{order}}
|
|
506
|
+
|
|
507
|
+
## Custom Collections
|
|
508
|
+
|
|
509
|
+
Tenants can create custom collections (e.g., "services", "testimonials", "products").
|
|
510
|
+
Token syntax is the same as built-in collections:
|
|
511
|
+
|
|
512
|
+
\`\`\`html
|
|
513
|
+
{{#each services}}
|
|
514
|
+
<h2>{{title}}</h2>
|
|
515
|
+
{{{description}}}
|
|
516
|
+
<img src="{{image}}">
|
|
517
|
+
{{/each}}
|
|
518
|
+
\`\`\`
|
|
519
|
+
|
|
520
|
+
**Built-in tokens for custom collections:**
|
|
521
|
+
- \`{{slug}}\` - Item URL slug
|
|
522
|
+
- \`{{url}}\` - Generated full URL
|
|
523
|
+
- \`{{publishedAt}}\` - Publish date (if enabled)
|
|
524
|
+
|
|
525
|
+
## Custom Fields on Built-in Collections
|
|
526
|
+
|
|
527
|
+
Tenants can add custom fields to Blog Posts, Authors, Team, and Downloads.
|
|
528
|
+
Use the same token syntax:
|
|
529
|
+
|
|
530
|
+
\`\`\`html
|
|
531
|
+
<!-- If "category" field was added to blogs -->
|
|
532
|
+
{{#each blogs}}
|
|
533
|
+
<span class="category">{{category}}</span>
|
|
534
|
+
{{/each}}
|
|
535
|
+
\`\`\`
|
|
536
|
+
|
|
537
|
+
**Use \`get_tenant_schema\` tool to see exact custom fields for a specific tenant.**`,
|
|
485
538
|
forms: `# Form Handling
|
|
486
539
|
|
|
487
540
|
Forms are automatically captured by the CMS.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GACZ,gBAAgB,GAChB,uBAAuB,GACvB,0BAA0B,GAC1B,qBAAqB,GACrB,oBAAoB,GACpB,eAAe,GACf,oBAAoB,GACpB,kBAAkB,GAClB,wBAAwB,GACxB,4BAA4B,GAC5B,eAAe,GACf,aAAa,GACb,gBAAgB,GAChB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GACZ,gBAAgB,GAChB,uBAAuB,GACvB,0BAA0B,GAC1B,qBAAqB,GACrB,oBAAoB,GACpB,eAAe,GACf,oBAAoB,GACpB,kBAAkB,GAClB,wBAAwB,GACxB,4BAA4B,GAC5B,eAAe,GACf,aAAa,GACb,gBAAgB,GAChB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,CAAC;AA61B1B;;GAEG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}
|
|
@@ -775,33 +775,47 @@ Inside loops, access the **parent scope** (the page's current item) using \`../\
|
|
|
775
775
|
|
|
776
776
|
Compare two values using \`(eq field1 field2)\` helper:
|
|
777
777
|
|
|
778
|
-
|
|
778
|
+
## Show When Equal ({{#if (eq ...)}})
|
|
779
779
|
\`\`\`html
|
|
780
780
|
{{#if (eq author.slug ../slug)}}
|
|
781
781
|
<span class="current-author-badge">✓ Your Post</span>
|
|
782
782
|
{{/if}}
|
|
783
|
+
\`\`\`
|
|
783
784
|
|
|
784
|
-
{{#
|
|
785
|
-
|
|
786
|
-
|
|
785
|
+
## Show When NOT Equal ({{#unless (eq ...)}}) - IMPORTANT!
|
|
786
|
+
The most common pattern for "Related Posts" or "Other Items" sections:
|
|
787
|
+
|
|
788
|
+
\`\`\`html
|
|
789
|
+
<!-- On a blog post page, show other posts EXCEPT the current one -->
|
|
790
|
+
<h3>Related Posts</h3>
|
|
791
|
+
{{#each blogs limit=3}}
|
|
792
|
+
{{#unless (eq slug ../slug)}}
|
|
793
|
+
<article>
|
|
794
|
+
<a href="{{url}}">{{name}}</a>
|
|
795
|
+
<p>{{postSummary}}</p>
|
|
796
|
+
</article>
|
|
797
|
+
{{/unless}}
|
|
798
|
+
{{/each}}
|
|
787
799
|
\`\`\`
|
|
788
800
|
|
|
789
|
-
**
|
|
801
|
+
**How it works:**
|
|
802
|
+
- \`../slug\` accesses the current page's slug (parent context)
|
|
803
|
+
- \`slug\` is the loop item's slug
|
|
804
|
+
- \`{{#unless (eq slug ../slug)}}\` shows content when they're NOT equal
|
|
805
|
+
- This excludes the current post from the "related" list
|
|
806
|
+
|
|
807
|
+
## Compare Field to Literal String ({{#eq}})
|
|
790
808
|
\`\`\`html
|
|
791
809
|
{{#eq status "published"}}
|
|
792
810
|
<span class="badge badge-success">Published</span>
|
|
793
811
|
{{/eq}}
|
|
794
812
|
|
|
795
|
-
{{#eq type "featured"}}
|
|
796
|
-
<div class="featured-highlight">⭐ {{name}}</div>
|
|
797
|
-
{{/eq}}
|
|
798
|
-
|
|
799
813
|
{{#eq category "news"}}
|
|
800
814
|
<span class="news-icon">📰</span>
|
|
801
815
|
{{/eq}}
|
|
802
816
|
\`\`\`
|
|
803
817
|
|
|
804
|
-
|
|
818
|
+
## Inside Loops with Parent Context
|
|
805
819
|
\`\`\`html
|
|
806
820
|
{{#each blogs}}
|
|
807
821
|
<article class="{{#if (eq author.name ../name)}}highlight{{/if}}">
|
|
@@ -813,11 +827,19 @@ Compare two values using \`(eq field1 field2)\` helper:
|
|
|
813
827
|
{{/each}}
|
|
814
828
|
\`\`\`
|
|
815
829
|
|
|
830
|
+
## Summary Table
|
|
831
|
+
|
|
832
|
+
| Syntax | Shows content when... |
|
|
833
|
+
|--------|----------------------|
|
|
834
|
+
| \`{{#if (eq a b)}}\` | a equals b |
|
|
835
|
+
| \`{{#unless (eq a b)}}\` | a does NOT equal b |
|
|
836
|
+
| \`{{#eq field "value"}}\` | field equals "value" |
|
|
837
|
+
|
|
816
838
|
**Common Use Cases:**
|
|
817
|
-
-
|
|
818
|
-
-
|
|
819
|
-
-
|
|
820
|
-
-
|
|
839
|
+
- Related posts (exclude current) - use \`{{#unless (eq slug ../slug)}}\`
|
|
840
|
+
- Filter by current author/category - use \`{{#if (eq author.slug ../slug)}}\`
|
|
841
|
+
- Highlight active items - use \`{{#if (eq ...)}}\`
|
|
842
|
+
- Show badges for specific statuses - use \`{{#eq status "value"}}\``,
|
|
821
843
|
};
|
|
822
844
|
/**
|
|
823
845
|
* Returns example code for a specific pattern
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-schema.d.ts","sourceRoot":"","sources":["../../src/tools/get-schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"get-schema.d.ts","sourceRoot":"","sources":["../../src/tools/get-schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAqZjD"}
|