@curenorway/kode-mcp 1.0.0 → 1.0.2
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 +32 -4
- package/dist/index.js +762 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ Run `kode init` in your project to create `.cure-kode/config.json`:
|
|
|
55
55
|
```bash
|
|
56
56
|
export CURE_KODE_API_KEY="ck_..."
|
|
57
57
|
export CURE_KODE_SITE_ID="your-site-uuid"
|
|
58
|
-
export CURE_KODE_API_URL="https://
|
|
58
|
+
export CURE_KODE_API_URL="https://app.cure.no" # optional
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
## Available Tools
|
|
@@ -109,10 +109,38 @@ Once configured, you can ask Claude:
|
|
|
109
109
|
|
|
110
110
|
## Security
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
-
|
|
112
|
+
### API Key Authentication
|
|
113
|
+
|
|
114
|
+
- **SHA256 Hashed Storage**: API keys are hashed before storage on the server
|
|
115
|
+
- **Site-scoped**: Each API key is bound to a specific CDN site
|
|
116
|
+
- **Permission-based**: Keys have granular permissions:
|
|
117
|
+
- `read` - List/view scripts and deployments
|
|
118
|
+
- `write` - Create and update scripts
|
|
119
|
+
- `deploy` - Deploy to staging/production
|
|
120
|
+
- `delete` - Delete scripts
|
|
121
|
+
- **Expiration**: Keys can have optional expiration dates
|
|
122
|
+
|
|
123
|
+
### Local Security
|
|
124
|
+
|
|
125
|
+
- API keys stored in `.cure-kode/config.json` (auto-gitignored)
|
|
115
126
|
- Never commit API keys to version control
|
|
127
|
+
- Keys prefixed with `ck_` for easy identification
|
|
128
|
+
|
|
129
|
+
### Network Security
|
|
130
|
+
|
|
131
|
+
- All API calls use HTTPS
|
|
132
|
+
- **SSRF Protection**: HTML fetch endpoints block:
|
|
133
|
+
- Private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
|
|
134
|
+
- Localhost and internal hostnames
|
|
135
|
+
- Cloud metadata endpoints (169.254.169.254)
|
|
136
|
+
- Only HTTP/HTTPS protocols allowed
|
|
137
|
+
|
|
138
|
+
### Best Practices
|
|
139
|
+
|
|
140
|
+
- Generate separate API keys for different environments/developers
|
|
141
|
+
- Use read-only keys when full access isn't needed
|
|
142
|
+
- Rotate keys periodically
|
|
143
|
+
- Revoke keys when team members leave
|
|
116
144
|
|
|
117
145
|
## Troubleshooting
|
|
118
146
|
|
package/dist/index.js
CHANGED
|
@@ -115,7 +115,7 @@ var KodeApiClient = class {
|
|
|
115
115
|
// src/config.ts
|
|
116
116
|
import * as fs from "fs";
|
|
117
117
|
import * as path from "path";
|
|
118
|
-
var DEFAULT_API_URL = "https://
|
|
118
|
+
var DEFAULT_API_URL = "https://app.cure.no";
|
|
119
119
|
var CONFIG_DIR = ".cure-kode";
|
|
120
120
|
var CONFIG_FILE = "config.json";
|
|
121
121
|
function findProjectRoot(startDir = process.cwd()) {
|
|
@@ -171,10 +171,30 @@ function getScriptsDir() {
|
|
|
171
171
|
if (!projectRoot || !projectConfig) return void 0;
|
|
172
172
|
return path.join(projectRoot, projectConfig.scriptsDir);
|
|
173
173
|
}
|
|
174
|
+
function getContextPath() {
|
|
175
|
+
const projectRoot = findProjectRoot();
|
|
176
|
+
if (!projectRoot) return void 0;
|
|
177
|
+
return path.join(projectRoot, CONFIG_DIR, "context.md");
|
|
178
|
+
}
|
|
179
|
+
function getPagesDir() {
|
|
180
|
+
const projectRoot = findProjectRoot();
|
|
181
|
+
if (!projectRoot) return void 0;
|
|
182
|
+
return path.join(projectRoot, CONFIG_DIR, "pages");
|
|
183
|
+
}
|
|
174
184
|
|
|
175
185
|
// src/index.ts
|
|
176
186
|
import * as fs2 from "fs";
|
|
177
187
|
import * as path2 from "path";
|
|
188
|
+
function urlToSlug(url) {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = new URL(url);
|
|
191
|
+
let slug = parsed.pathname.replace(/^\//, "").replace(/\//g, "--") || "home";
|
|
192
|
+
slug = slug.replace(/--$/, "");
|
|
193
|
+
return slug.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
|
|
194
|
+
} catch {
|
|
195
|
+
return "page";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
178
198
|
var server = new Server(
|
|
179
199
|
{
|
|
180
200
|
name: "cure-kode",
|
|
@@ -365,6 +385,123 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
365
385
|
properties: {},
|
|
366
386
|
required: []
|
|
367
387
|
}
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: "kode_read_context",
|
|
391
|
+
description: "Read the project context file (.cure-kode/context.md). Contains current scripts, notes, and session history. ALWAYS call this before starting work on a Kode project.",
|
|
392
|
+
inputSchema: {
|
|
393
|
+
type: "object",
|
|
394
|
+
properties: {},
|
|
395
|
+
required: []
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "kode_update_context",
|
|
400
|
+
description: "Update the project context file. Use this to add notes, record session history, or update script purposes. Call this after making changes to persist your discoveries.",
|
|
401
|
+
inputSchema: {
|
|
402
|
+
type: "object",
|
|
403
|
+
properties: {
|
|
404
|
+
addNote: {
|
|
405
|
+
type: "string",
|
|
406
|
+
description: "Add a note to the notes section (e.g., HTML structure discovery, third-party integration found)"
|
|
407
|
+
},
|
|
408
|
+
addSession: {
|
|
409
|
+
type: "object",
|
|
410
|
+
description: "Record a work session",
|
|
411
|
+
properties: {
|
|
412
|
+
task: {
|
|
413
|
+
type: "string",
|
|
414
|
+
description: "What task was performed"
|
|
415
|
+
},
|
|
416
|
+
changes: {
|
|
417
|
+
type: "array",
|
|
418
|
+
items: { type: "string" },
|
|
419
|
+
description: "List of changes made"
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
required: ["task", "changes"]
|
|
423
|
+
},
|
|
424
|
+
updateScriptPurpose: {
|
|
425
|
+
type: "object",
|
|
426
|
+
description: "Update a script's purpose description",
|
|
427
|
+
properties: {
|
|
428
|
+
slug: {
|
|
429
|
+
type: "string",
|
|
430
|
+
description: "Script slug"
|
|
431
|
+
},
|
|
432
|
+
purpose: {
|
|
433
|
+
type: "string",
|
|
434
|
+
description: "Purpose description"
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
required: ["slug", "purpose"]
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
required: []
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: "kode_fetch_html_smart",
|
|
445
|
+
description: "Fetch and analyze a webpage with smart CMS truncation. Shows template pattern instead of all CMS items. Better for understanding page structure.",
|
|
446
|
+
inputSchema: {
|
|
447
|
+
type: "object",
|
|
448
|
+
properties: {
|
|
449
|
+
url: {
|
|
450
|
+
type: "string",
|
|
451
|
+
description: 'Full URL to analyze (e.g., "https://example.com")'
|
|
452
|
+
},
|
|
453
|
+
truncateCms: {
|
|
454
|
+
type: "boolean",
|
|
455
|
+
description: "Truncate CMS collections to show template pattern only. Default: true"
|
|
456
|
+
},
|
|
457
|
+
includeHtml: {
|
|
458
|
+
type: "boolean",
|
|
459
|
+
description: "Include HTML preview in response. Default: false (returns structure only)"
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
required: ["url"]
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: "kode_refresh_page",
|
|
467
|
+
description: 'Fetch webpage and extract FULL structure with CSS selectors. WHEN TO USE: (1) User says "HTML changed" or "refresh", (2) Cache is >7 days old, (3) First time analyzing a page, (4) Before writing code that targets elements. Returns: sections, CTAs, forms, CMS, headings, scripts, images, embeds, interactions - all with CSS selectors for development.',
|
|
468
|
+
inputSchema: {
|
|
469
|
+
type: "object",
|
|
470
|
+
properties: {
|
|
471
|
+
url: {
|
|
472
|
+
type: "string",
|
|
473
|
+
description: 'Full URL to analyze and cache (e.g., "https://example.com/about")'
|
|
474
|
+
},
|
|
475
|
+
force: {
|
|
476
|
+
type: "boolean",
|
|
477
|
+
description: "Force refresh even if cached. Use when user says HTML has changed."
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
required: ["url"]
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
name: "kode_get_page_context",
|
|
485
|
+
description: "Get FULL page context with CSS selectors for development. Returns: sections with selectors, navigation, CTAs with classes, forms with field selectors, CMS patterns, headings, scripts inventory, images, embeds, and Webflow interactions. Warns if cache is >7 days old. Use kode_refresh_page if HTML has changed or cache is stale.",
|
|
486
|
+
inputSchema: {
|
|
487
|
+
type: "object",
|
|
488
|
+
properties: {
|
|
489
|
+
urlOrSlug: {
|
|
490
|
+
type: "string",
|
|
491
|
+
description: 'URL or slug of the cached page (e.g., "https://example.com/about" or "about")'
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
required: ["urlOrSlug"]
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "kode_list_pages_context",
|
|
499
|
+
description: "List all cached page contexts. Shows which pages have been analyzed and when. Use kode_refresh_page to add new pages.",
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: "object",
|
|
502
|
+
properties: {},
|
|
503
|
+
required: []
|
|
504
|
+
}
|
|
368
505
|
}
|
|
369
506
|
]
|
|
370
507
|
};
|
|
@@ -665,6 +802,630 @@ Embed code:
|
|
|
665
802
|
content: [{ type: "text", text }]
|
|
666
803
|
};
|
|
667
804
|
}
|
|
805
|
+
case "kode_read_context": {
|
|
806
|
+
const contextPath = getContextPath();
|
|
807
|
+
if (!contextPath) {
|
|
808
|
+
return {
|
|
809
|
+
content: [{ type: "text", text: 'Not in a Cure Kode project. Run "kode init" first.' }],
|
|
810
|
+
isError: true
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
if (!fs2.existsSync(contextPath)) {
|
|
814
|
+
return {
|
|
815
|
+
content: [{ type: "text", text: 'No context file found. Run "kode context --refresh" to create one.' }],
|
|
816
|
+
isError: true
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
const content = fs2.readFileSync(contextPath, "utf-8");
|
|
820
|
+
return {
|
|
821
|
+
content: [{ type: "text", text: content }]
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
case "kode_update_context": {
|
|
825
|
+
const contextPath = getContextPath();
|
|
826
|
+
if (!contextPath) {
|
|
827
|
+
return {
|
|
828
|
+
content: [{ type: "text", text: 'Not in a Cure Kode project. Run "kode init" first.' }],
|
|
829
|
+
isError: true
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
if (!fs2.existsSync(contextPath)) {
|
|
833
|
+
return {
|
|
834
|
+
content: [{ type: "text", text: 'No context file found. Run "kode context --refresh" to create one.' }],
|
|
835
|
+
isError: true
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
const { addNote, addSession, updateScriptPurpose } = args;
|
|
839
|
+
let content = fs2.readFileSync(contextPath, "utf-8");
|
|
840
|
+
const updates = [];
|
|
841
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
842
|
+
content = content.replace(
|
|
843
|
+
/> Last updated: .+$/m,
|
|
844
|
+
`> Last updated: ${timestamp}`
|
|
845
|
+
);
|
|
846
|
+
content = content.replace(
|
|
847
|
+
/> Updated by: .+$/m,
|
|
848
|
+
"> Updated by: Claude (AI Agent)"
|
|
849
|
+
);
|
|
850
|
+
if (addNote) {
|
|
851
|
+
const notesMatch = content.match(/## Notes\n\n((?:- .+\n?)*)/m);
|
|
852
|
+
if (notesMatch) {
|
|
853
|
+
const existingNotes = notesMatch[1];
|
|
854
|
+
const cleanedNotes = existingNotes.split("\n").filter((line) => line && !line.includes("(HTML structure") && !line.includes("(Third-party") && !line.includes("(Known issues")).join("\n");
|
|
855
|
+
const newNotes = cleanedNotes ? `${cleanedNotes}
|
|
856
|
+
- ${addNote}
|
|
857
|
+
` : `- ${addNote}
|
|
858
|
+
`;
|
|
859
|
+
content = content.replace(
|
|
860
|
+
/## Notes\n\n(?:- .+\n?)*/m,
|
|
861
|
+
`## Notes
|
|
862
|
+
|
|
863
|
+
${newNotes}`
|
|
864
|
+
);
|
|
865
|
+
updates.push(`Added note: "${addNote}"`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (addSession) {
|
|
869
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
870
|
+
const sessionMd = `### ${date} - Claude
|
|
871
|
+
**Task**: ${addSession.task}
|
|
872
|
+
**Changes**:
|
|
873
|
+
${addSession.changes.map((c) => `- ${c}`).join("\n")}
|
|
874
|
+
|
|
875
|
+
`;
|
|
876
|
+
const sessionsMatch = content.match(/## Sessions\n\n/);
|
|
877
|
+
if (sessionsMatch) {
|
|
878
|
+
content = content.replace(
|
|
879
|
+
/## Sessions\n\n/m,
|
|
880
|
+
`## Sessions
|
|
881
|
+
|
|
882
|
+
${sessionMd}`
|
|
883
|
+
);
|
|
884
|
+
content = content.replace(/\(No sessions recorded yet\)\n?/m, "");
|
|
885
|
+
updates.push(`Added session: "${addSession.task}"`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (updateScriptPurpose) {
|
|
889
|
+
const { slug, purpose } = updateScriptPurpose;
|
|
890
|
+
const rowRegex = new RegExp(`\\| ${slug} \\| (\\w+) \\| ([\\w-]+) \\| [^|]* \\|`, "m");
|
|
891
|
+
const match = content.match(rowRegex);
|
|
892
|
+
if (match) {
|
|
893
|
+
content = content.replace(
|
|
894
|
+
rowRegex,
|
|
895
|
+
`| ${slug} | ${match[1]} | ${match[2]} | ${purpose} |`
|
|
896
|
+
);
|
|
897
|
+
updates.push(`Updated purpose for "${slug}": "${purpose}"`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
fs2.writeFileSync(contextPath, content, "utf-8");
|
|
901
|
+
return {
|
|
902
|
+
content: [
|
|
903
|
+
{
|
|
904
|
+
type: "text",
|
|
905
|
+
text: updates.length > 0 ? `Context updated:
|
|
906
|
+
${updates.map((u) => `- ${u}`).join("\n")}` : "No updates applied"
|
|
907
|
+
}
|
|
908
|
+
]
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
case "kode_fetch_html_smart": {
|
|
912
|
+
const { url, truncateCms = true, includeHtml = false } = args;
|
|
913
|
+
const config = getConfig();
|
|
914
|
+
if (!config) {
|
|
915
|
+
return {
|
|
916
|
+
content: [{ type: "text", text: "Cure Kode not configured" }],
|
|
917
|
+
isError: true
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const response = await fetch(`${config.apiUrl}/api/cdn/fetch-html`, {
|
|
921
|
+
method: "POST",
|
|
922
|
+
headers: {
|
|
923
|
+
"Content-Type": "application/json",
|
|
924
|
+
"X-API-Key": config.apiKey
|
|
925
|
+
},
|
|
926
|
+
body: JSON.stringify({ url, truncateCms })
|
|
927
|
+
});
|
|
928
|
+
if (!response.ok) {
|
|
929
|
+
return {
|
|
930
|
+
content: [{ type: "text", text: `Failed to fetch HTML: ${response.statusText}` }],
|
|
931
|
+
isError: true
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const result = await response.json();
|
|
935
|
+
let text = `URL: ${result.url}
|
|
936
|
+
Title: ${result.title || "(no title)"}
|
|
937
|
+
|
|
938
|
+
`;
|
|
939
|
+
text += `Scripts (${result.stats.totalScripts} total):
|
|
940
|
+
`;
|
|
941
|
+
if (result.scripts.webflow.length > 0) {
|
|
942
|
+
text += ` Webflow: ${result.scripts.webflow.length}
|
|
943
|
+
`;
|
|
944
|
+
}
|
|
945
|
+
if (result.scripts.cureKode.length > 0) {
|
|
946
|
+
text += ` Cure Kode: ${result.scripts.cureKode.length}
|
|
947
|
+
`;
|
|
948
|
+
}
|
|
949
|
+
if (result.scripts.thirdParty.length > 0) {
|
|
950
|
+
text += ` Third-party: ${result.scripts.thirdParty.length}
|
|
951
|
+
`;
|
|
952
|
+
for (const script of result.scripts.thirdParty.slice(0, 5)) {
|
|
953
|
+
text += ` - ${script.type || script.src || "(inline)"}
|
|
954
|
+
`;
|
|
955
|
+
}
|
|
956
|
+
if (result.scripts.thirdParty.length > 5) {
|
|
957
|
+
text += ` ... and ${result.scripts.thirdParty.length - 5} more
|
|
958
|
+
`;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (result.scripts.custom.length > 0) {
|
|
962
|
+
text += ` Custom: ${result.scripts.custom.length}
|
|
963
|
+
`;
|
|
964
|
+
}
|
|
965
|
+
text += "\n";
|
|
966
|
+
text += `Styles: ${result.stats.totalStyles}
|
|
967
|
+
|
|
968
|
+
`;
|
|
969
|
+
if (result.detectedComponents.length > 0) {
|
|
970
|
+
text += `Webflow Components:
|
|
971
|
+
`;
|
|
972
|
+
for (const comp of result.detectedComponents) {
|
|
973
|
+
text += ` - ${comp}
|
|
974
|
+
`;
|
|
975
|
+
}
|
|
976
|
+
text += "\n";
|
|
977
|
+
}
|
|
978
|
+
if (result.cmsCollections && result.cmsCollections.length > 0) {
|
|
979
|
+
text += `CMS Collections (truncated):
|
|
980
|
+
`;
|
|
981
|
+
for (const collection of result.cmsCollections) {
|
|
982
|
+
text += ` - ${collection.selector}: ${collection.itemCount} items
|
|
983
|
+
`;
|
|
984
|
+
}
|
|
985
|
+
text += "\n";
|
|
986
|
+
}
|
|
987
|
+
if (includeHtml && result.htmlPreview) {
|
|
988
|
+
text += `HTML Preview (first 5000 chars):
|
|
989
|
+
${result.htmlPreview.slice(0, 5e3)}`;
|
|
990
|
+
if (result.htmlPreview.length > 5e3) {
|
|
991
|
+
text += "\n... (truncated)";
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
content: [{ type: "text", text }]
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
case "kode_refresh_page": {
|
|
999
|
+
const { url, force = false } = args;
|
|
1000
|
+
const pagesDir = getPagesDir();
|
|
1001
|
+
if (!pagesDir) {
|
|
1002
|
+
return {
|
|
1003
|
+
content: [{ type: "text", text: 'Not in a Cure Kode project. Run "kode init" first.' }],
|
|
1004
|
+
isError: true
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const slug = urlToSlug(url);
|
|
1008
|
+
const cachePath = path2.join(pagesDir, `${slug}.json`);
|
|
1009
|
+
if (!force && fs2.existsSync(cachePath)) {
|
|
1010
|
+
try {
|
|
1011
|
+
const cached = JSON.parse(fs2.readFileSync(cachePath, "utf-8"));
|
|
1012
|
+
return {
|
|
1013
|
+
content: [{
|
|
1014
|
+
type: "text",
|
|
1015
|
+
text: `Using cached version from ${cached.extractedAt}
|
|
1016
|
+
Slug: ${slug}
|
|
1017
|
+
Use force: true to refresh.
|
|
1018
|
+
|
|
1019
|
+
Page: ${cached.title || url}
|
|
1020
|
+
Sections: ${cached.sections.length}
|
|
1021
|
+
CTAs: ${cached.ctas.length}
|
|
1022
|
+
Forms: ${cached.forms.length}
|
|
1023
|
+
CMS Collections: ${cached.cmsPatterns.length}`
|
|
1024
|
+
}]
|
|
1025
|
+
};
|
|
1026
|
+
} catch {
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const config = getConfig();
|
|
1030
|
+
if (!config) {
|
|
1031
|
+
return {
|
|
1032
|
+
content: [{ type: "text", text: "Cure Kode not configured" }],
|
|
1033
|
+
isError: true
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
const response = await fetch(`${config.apiUrl}/api/cdn/page-context`, {
|
|
1037
|
+
method: "POST",
|
|
1038
|
+
headers: {
|
|
1039
|
+
"Content-Type": "application/json",
|
|
1040
|
+
"X-API-Key": config.apiKey
|
|
1041
|
+
},
|
|
1042
|
+
body: JSON.stringify({ url })
|
|
1043
|
+
});
|
|
1044
|
+
if (!response.ok) {
|
|
1045
|
+
return {
|
|
1046
|
+
content: [{ type: "text", text: `Failed to fetch page: ${response.statusText}` }],
|
|
1047
|
+
isError: true
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
const structure = await response.json();
|
|
1051
|
+
if (!fs2.existsSync(pagesDir)) {
|
|
1052
|
+
fs2.mkdirSync(pagesDir, { recursive: true });
|
|
1053
|
+
}
|
|
1054
|
+
fs2.writeFileSync(cachePath, JSON.stringify(structure, null, 2), "utf-8");
|
|
1055
|
+
const contextPath = getContextPath();
|
|
1056
|
+
if (contextPath && fs2.existsSync(contextPath)) {
|
|
1057
|
+
}
|
|
1058
|
+
let text = `Page cached: ${slug}
|
|
1059
|
+
Path: ${cachePath}
|
|
1060
|
+
|
|
1061
|
+
`;
|
|
1062
|
+
text += `Title: ${structure.title || "(no title)"}
|
|
1063
|
+
`;
|
|
1064
|
+
text += `URL: ${structure.url}
|
|
1065
|
+
|
|
1066
|
+
`;
|
|
1067
|
+
text += `Sections (${structure.sections.length}):
|
|
1068
|
+
`;
|
|
1069
|
+
for (const s of structure.sections.slice(0, 6)) {
|
|
1070
|
+
const name2 = s.heading || s.id || s.className?.split(" ")[0] || "section";
|
|
1071
|
+
const flags = [s.hasCms && "CMS", s.hasForm && "Form"].filter(Boolean).join(", ");
|
|
1072
|
+
text += ` - ${name2}${flags ? ` [${flags}]` : ""}
|
|
1073
|
+
`;
|
|
1074
|
+
}
|
|
1075
|
+
if (structure.sections.length > 6) {
|
|
1076
|
+
text += ` ... and ${structure.sections.length - 6} more
|
|
1077
|
+
`;
|
|
1078
|
+
}
|
|
1079
|
+
if (structure.ctas.length > 0) {
|
|
1080
|
+
text += `
|
|
1081
|
+
CTAs (${structure.ctas.length}):
|
|
1082
|
+
`;
|
|
1083
|
+
for (const c of structure.ctas.slice(0, 4)) {
|
|
1084
|
+
text += ` - "${c.text}"
|
|
1085
|
+
`;
|
|
1086
|
+
}
|
|
1087
|
+
if (structure.ctas.length > 4) {
|
|
1088
|
+
text += ` ... and ${structure.ctas.length - 4} more
|
|
1089
|
+
`;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (structure.forms.length > 0) {
|
|
1093
|
+
text += `
|
|
1094
|
+
Forms (${structure.forms.length}):
|
|
1095
|
+
`;
|
|
1096
|
+
for (const f of structure.forms) {
|
|
1097
|
+
text += ` - ${f.name || "form"}: ${f.fields.length} fields
|
|
1098
|
+
`;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (structure.cmsPatterns.length > 0) {
|
|
1102
|
+
text += `
|
|
1103
|
+
CMS Collections (${structure.cmsPatterns.length}):
|
|
1104
|
+
`;
|
|
1105
|
+
for (const c of structure.cmsPatterns) {
|
|
1106
|
+
text += ` - ${c.containerSelector}: ${c.itemCount} items
|
|
1107
|
+
`;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
content: [{ type: "text", text }]
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
case "kode_get_page_context": {
|
|
1115
|
+
const { urlOrSlug } = args;
|
|
1116
|
+
const pagesDir = getPagesDir();
|
|
1117
|
+
if (!pagesDir) {
|
|
1118
|
+
return {
|
|
1119
|
+
content: [{ type: "text", text: 'Not in a Cure Kode project. Run "kode init" first.' }],
|
|
1120
|
+
isError: true
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
const slug = urlOrSlug.startsWith("http") ? urlToSlug(urlOrSlug) : urlOrSlug;
|
|
1124
|
+
const cachePath = path2.join(pagesDir, `${slug}.json`);
|
|
1125
|
+
if (!fs2.existsSync(cachePath)) {
|
|
1126
|
+
return {
|
|
1127
|
+
content: [{
|
|
1128
|
+
type: "text",
|
|
1129
|
+
text: `Page not found: ${slug}
|
|
1130
|
+
|
|
1131
|
+
Use kode_list_pages_context to see cached pages, or kode_refresh_page to cache a new page.`
|
|
1132
|
+
}],
|
|
1133
|
+
isError: true
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
const context = JSON.parse(fs2.readFileSync(cachePath, "utf-8"));
|
|
1137
|
+
const cacheAge = Date.now() - new Date(context.extractedAt).getTime();
|
|
1138
|
+
const cacheAgeDays = Math.floor(cacheAge / (1e3 * 60 * 60 * 24));
|
|
1139
|
+
const isStale = cacheAgeDays > 7;
|
|
1140
|
+
let text = `# ${context.title || context.url}
|
|
1141
|
+
|
|
1142
|
+
`;
|
|
1143
|
+
text += `URL: ${context.url}
|
|
1144
|
+
`;
|
|
1145
|
+
text += `Cached: ${context.extractedAt}${isStale ? ` \u26A0\uFE0F (${cacheAgeDays} days old - consider refreshing with kode_refresh_page)` : ""}
|
|
1146
|
+
`;
|
|
1147
|
+
if (context.stats) {
|
|
1148
|
+
text += `Stats: ${context.stats.sectionCount} sections, ${context.stats.ctaCount} CTAs, ${context.stats.formCount} forms, ${context.stats.linkCount} links, ${context.stats.gridCount || 0} grids, ${context.stats.imageCount} images, ${context.stats.interactiveCount || 0} interactive
|
|
1149
|
+
`;
|
|
1150
|
+
}
|
|
1151
|
+
text += "\n";
|
|
1152
|
+
if (context.sections && context.sections.length > 0) {
|
|
1153
|
+
text += `## Sections (${context.sections.length})
|
|
1154
|
+
|
|
1155
|
+
`;
|
|
1156
|
+
for (const s of context.sections) {
|
|
1157
|
+
const flags = [s.hasCms && "CMS", s.hasForm && "Form", s.webflowInteraction && "Interaction"].filter(Boolean).join(", ");
|
|
1158
|
+
text += `### ${s.heading || s.selector}
|
|
1159
|
+
`;
|
|
1160
|
+
text += `Selector: \`${s.selector}\`
|
|
1161
|
+
`;
|
|
1162
|
+
if (flags) text += `Flags: [${flags}]
|
|
1163
|
+
`;
|
|
1164
|
+
if (s.children) {
|
|
1165
|
+
const c = s.children;
|
|
1166
|
+
const childInfo = [
|
|
1167
|
+
c.divs && `${c.divs} divs`,
|
|
1168
|
+
c.links && `${c.links} links`,
|
|
1169
|
+
c.buttons && `${c.buttons} buttons`,
|
|
1170
|
+
c.images && `${c.images} images`
|
|
1171
|
+
].filter(Boolean).join(", ");
|
|
1172
|
+
if (childInfo) text += `Contains: ${childInfo}
|
|
1173
|
+
`;
|
|
1174
|
+
}
|
|
1175
|
+
if (s.textSample) text += `Preview: ${s.textSample.slice(0, 120)}...
|
|
1176
|
+
`;
|
|
1177
|
+
text += "\n";
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (context.navigation && context.navigation.length > 0) {
|
|
1181
|
+
text += `## Navigation
|
|
1182
|
+
|
|
1183
|
+
`;
|
|
1184
|
+
for (const n of context.navigation) {
|
|
1185
|
+
text += `### ${n.type} nav
|
|
1186
|
+
`;
|
|
1187
|
+
text += `Selector: \`${n.selector}\`
|
|
1188
|
+
`;
|
|
1189
|
+
for (const item of n.items.slice(0, 10)) {
|
|
1190
|
+
text += ` - "${item.text}" \`${item.selector}\`${item.href ? ` \u2192 ${item.href}` : ""}
|
|
1191
|
+
`;
|
|
1192
|
+
}
|
|
1193
|
+
text += "\n";
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (context.ctas && context.ctas.length > 0) {
|
|
1197
|
+
text += `## CTAs (${context.ctas.length})
|
|
1198
|
+
|
|
1199
|
+
`;
|
|
1200
|
+
for (const c of context.ctas) {
|
|
1201
|
+
text += `- "${c.text}" \`${c.selector}\`${c.href ? ` \u2192 ${c.href}` : ""}
|
|
1202
|
+
`;
|
|
1203
|
+
if (c.classes && c.classes.length > 0) {
|
|
1204
|
+
text += ` Classes: ${c.classes.join(", ")}
|
|
1205
|
+
`;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
text += "\n";
|
|
1209
|
+
}
|
|
1210
|
+
if (context.forms && context.forms.length > 0) {
|
|
1211
|
+
text += `## Forms (${context.forms.length})
|
|
1212
|
+
|
|
1213
|
+
`;
|
|
1214
|
+
for (const f of context.forms) {
|
|
1215
|
+
text += `### ${f.name || f.id || "Form"}
|
|
1216
|
+
`;
|
|
1217
|
+
text += `Selector: \`${f.selector}\`
|
|
1218
|
+
`;
|
|
1219
|
+
if (f.action) text += `Action: ${f.action} (${f.method || "GET"})
|
|
1220
|
+
`;
|
|
1221
|
+
text += `Fields:
|
|
1222
|
+
`;
|
|
1223
|
+
for (const field of f.fields) {
|
|
1224
|
+
const req = field.required ? " *" : "";
|
|
1225
|
+
text += ` - ${field.label || field.name || field.type}${req}: \`${field.selector}\`
|
|
1226
|
+
`;
|
|
1227
|
+
}
|
|
1228
|
+
if (f.submitButton) {
|
|
1229
|
+
text += `Submit: "${f.submitButton.text}" \`${f.submitButton.selector}\`
|
|
1230
|
+
`;
|
|
1231
|
+
}
|
|
1232
|
+
text += "\n";
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (context.cmsPatterns && context.cmsPatterns.length > 0) {
|
|
1236
|
+
text += `## CMS Collections (${context.cmsPatterns.length})
|
|
1237
|
+
|
|
1238
|
+
`;
|
|
1239
|
+
for (const c of context.cmsPatterns) {
|
|
1240
|
+
text += `Container: \`${c.containerSelector}\` (${c.itemCount} items)
|
|
1241
|
+
`;
|
|
1242
|
+
text += `Item: \`${c.itemSelector}\`
|
|
1243
|
+
`;
|
|
1244
|
+
if (c.templateFields && c.templateFields.length > 0) {
|
|
1245
|
+
text += `Template fields: ${c.templateFields.join(", ")}
|
|
1246
|
+
`;
|
|
1247
|
+
}
|
|
1248
|
+
text += "\n";
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (context.headings && context.headings.length > 0) {
|
|
1252
|
+
text += `## Headings (${context.headings.length})
|
|
1253
|
+
|
|
1254
|
+
`;
|
|
1255
|
+
for (const h of context.headings.slice(0, 15)) {
|
|
1256
|
+
const indent = " ".repeat(h.level - 1);
|
|
1257
|
+
text += `${indent}H${h.level}: "${h.text}" \`${h.selector}\`
|
|
1258
|
+
`;
|
|
1259
|
+
}
|
|
1260
|
+
if (context.headings.length > 15) {
|
|
1261
|
+
text += `... and ${context.headings.length - 15} more
|
|
1262
|
+
`;
|
|
1263
|
+
}
|
|
1264
|
+
text += "\n";
|
|
1265
|
+
}
|
|
1266
|
+
if (context.scripts && context.scripts.length > 0) {
|
|
1267
|
+
text += `## Scripts (${context.scripts.length})
|
|
1268
|
+
|
|
1269
|
+
`;
|
|
1270
|
+
const byCategory = {};
|
|
1271
|
+
for (const s of context.scripts) {
|
|
1272
|
+
byCategory[s.category] = byCategory[s.category] || [];
|
|
1273
|
+
byCategory[s.category].push(s);
|
|
1274
|
+
}
|
|
1275
|
+
for (const [cat, scripts] of Object.entries(byCategory)) {
|
|
1276
|
+
text += `### ${cat} (${scripts.length})
|
|
1277
|
+
`;
|
|
1278
|
+
for (const s of scripts.slice(0, 5)) {
|
|
1279
|
+
if (s.src) {
|
|
1280
|
+
text += ` - ${s.src}${s.async ? " (async)" : ""}${s.defer ? " (defer)" : ""}
|
|
1281
|
+
`;
|
|
1282
|
+
} else {
|
|
1283
|
+
text += ` - [inline ${s.type}] ${s.contentPreview?.slice(0, 50)}...
|
|
1284
|
+
`;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
if (scripts.length > 5) text += ` ... and ${scripts.length - 5} more
|
|
1288
|
+
`;
|
|
1289
|
+
}
|
|
1290
|
+
text += "\n";
|
|
1291
|
+
}
|
|
1292
|
+
if (context.images && context.images.length > 0) {
|
|
1293
|
+
text += `## Images (${context.images.length})
|
|
1294
|
+
|
|
1295
|
+
`;
|
|
1296
|
+
for (const img of context.images.slice(0, 10)) {
|
|
1297
|
+
const dims = img.dimensions ? ` (${img.dimensions.width}\xD7${img.dimensions.height})` : "";
|
|
1298
|
+
text += `- "${img.alt || "no alt"}" \`${img.selector}\`${dims}
|
|
1299
|
+
`;
|
|
1300
|
+
}
|
|
1301
|
+
if (context.images.length > 10) {
|
|
1302
|
+
text += `... and ${context.images.length - 10} more
|
|
1303
|
+
`;
|
|
1304
|
+
}
|
|
1305
|
+
text += "\n";
|
|
1306
|
+
}
|
|
1307
|
+
if (context.webflowInteractions && context.webflowInteractions.length > 0) {
|
|
1308
|
+
text += `## Webflow Interactions (${context.webflowInteractions.length})
|
|
1309
|
+
|
|
1310
|
+
`;
|
|
1311
|
+
for (const i of context.webflowInteractions) {
|
|
1312
|
+
text += `- \`${i.triggerSelector}\`${i.type ? ` (${i.type})` : ""}
|
|
1313
|
+
`;
|
|
1314
|
+
}
|
|
1315
|
+
text += "\n";
|
|
1316
|
+
}
|
|
1317
|
+
if (context.embeds && context.embeds.length > 0) {
|
|
1318
|
+
text += `## Custom Embeds (${context.embeds.length})
|
|
1319
|
+
|
|
1320
|
+
`;
|
|
1321
|
+
for (const e of context.embeds) {
|
|
1322
|
+
text += `- ${e.type} embed \`${e.selector}\` in ${e.location}
|
|
1323
|
+
`;
|
|
1324
|
+
}
|
|
1325
|
+
text += "\n";
|
|
1326
|
+
}
|
|
1327
|
+
if (context.grids && context.grids.length > 0) {
|
|
1328
|
+
text += `## Grids/Lists (${context.grids.length})
|
|
1329
|
+
|
|
1330
|
+
`;
|
|
1331
|
+
for (const g of context.grids) {
|
|
1332
|
+
text += `- \`${g.selector}\` (${g.gridType}, ${g.itemCount} items)
|
|
1333
|
+
`;
|
|
1334
|
+
text += ` Item: \`${g.itemSelector}\`
|
|
1335
|
+
`;
|
|
1336
|
+
}
|
|
1337
|
+
text += "\n";
|
|
1338
|
+
}
|
|
1339
|
+
if (context.interactiveElements && context.interactiveElements.length > 0) {
|
|
1340
|
+
text += `## Interactive Elements (${context.interactiveElements.length})
|
|
1341
|
+
|
|
1342
|
+
`;
|
|
1343
|
+
for (const el of context.interactiveElements) {
|
|
1344
|
+
text += `- ${el.type}: \`${el.selector}\`${el.trigger ? ` [${el.trigger}]` : ""}
|
|
1345
|
+
`;
|
|
1346
|
+
}
|
|
1347
|
+
text += "\n";
|
|
1348
|
+
}
|
|
1349
|
+
if (context.links && context.links.length > 0) {
|
|
1350
|
+
const internal = context.links.filter((l) => !l.isExternal).length;
|
|
1351
|
+
const external = context.links.filter((l) => l.isExternal).length;
|
|
1352
|
+
text += `## Links (${context.links.length})
|
|
1353
|
+
|
|
1354
|
+
`;
|
|
1355
|
+
text += `Internal: ${internal}, External: ${external}
|
|
1356
|
+
|
|
1357
|
+
`;
|
|
1358
|
+
}
|
|
1359
|
+
if (context.notes && context.notes.length > 0) {
|
|
1360
|
+
text += `## Notes
|
|
1361
|
+
|
|
1362
|
+
`;
|
|
1363
|
+
for (const note of context.notes) {
|
|
1364
|
+
text += `- ${note}
|
|
1365
|
+
`;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return {
|
|
1369
|
+
content: [{ type: "text", text }]
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
case "kode_list_pages_context": {
|
|
1373
|
+
const pagesDir = getPagesDir();
|
|
1374
|
+
if (!pagesDir) {
|
|
1375
|
+
return {
|
|
1376
|
+
content: [{ type: "text", text: 'Not in a Cure Kode project. Run "kode init" first.' }],
|
|
1377
|
+
isError: true
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
if (!fs2.existsSync(pagesDir)) {
|
|
1381
|
+
return {
|
|
1382
|
+
content: [{
|
|
1383
|
+
type: "text",
|
|
1384
|
+
text: "No cached pages.\n\nUse kode_refresh_page to analyze and cache a page's structure."
|
|
1385
|
+
}]
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
const files = fs2.readdirSync(pagesDir).filter((f) => f.endsWith(".json"));
|
|
1389
|
+
if (files.length === 0) {
|
|
1390
|
+
return {
|
|
1391
|
+
content: [{
|
|
1392
|
+
type: "text",
|
|
1393
|
+
text: "No cached pages.\n\nUse kode_refresh_page to analyze and cache a page's structure."
|
|
1394
|
+
}]
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
let text = `Cached Pages (${files.length})
|
|
1398
|
+
|
|
1399
|
+
`;
|
|
1400
|
+
for (const file of files) {
|
|
1401
|
+
const slug = file.replace(".json", "");
|
|
1402
|
+
try {
|
|
1403
|
+
const context = JSON.parse(fs2.readFileSync(path2.join(pagesDir, file), "utf-8"));
|
|
1404
|
+
const urlPath = new URL(context.url).pathname;
|
|
1405
|
+
text += `${urlPath} [${slug}]
|
|
1406
|
+
`;
|
|
1407
|
+
if (context.title) {
|
|
1408
|
+
text += ` "${context.title}"
|
|
1409
|
+
`;
|
|
1410
|
+
}
|
|
1411
|
+
text += ` Sections: ${context.sections.length}, CTAs: ${context.ctas.length}, Forms: ${context.forms.length}, CMS: ${context.cmsPatterns.length}
|
|
1412
|
+
`;
|
|
1413
|
+
text += ` Cached: ${context.extractedAt}
|
|
1414
|
+
|
|
1415
|
+
`;
|
|
1416
|
+
} catch {
|
|
1417
|
+
text += `${slug} (invalid cache file)
|
|
1418
|
+
|
|
1419
|
+
`;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
text += `Use kode_get_page_context to see full details for a page.
|
|
1423
|
+
`;
|
|
1424
|
+
text += `Use kode_refresh_page to update or add pages.`;
|
|
1425
|
+
return {
|
|
1426
|
+
content: [{ type: "text", text }]
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
668
1429
|
default:
|
|
669
1430
|
return {
|
|
670
1431
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|