community-ff-mcp 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +300 -0
  2. package/build/api/flutterflow.d.ts +11 -0
  3. package/build/api/flutterflow.js +61 -0
  4. package/build/index.d.ts +2 -0
  5. package/build/index.js +78 -0
  6. package/build/prompts/dev-workflow.d.ts +2 -0
  7. package/build/prompts/dev-workflow.js +68 -0
  8. package/build/prompts/generate-page.d.ts +2 -0
  9. package/build/prompts/generate-page.js +37 -0
  10. package/build/prompts/inspect-project.d.ts +2 -0
  11. package/build/prompts/inspect-project.js +30 -0
  12. package/build/prompts/modify-component.d.ts +2 -0
  13. package/build/prompts/modify-component.js +40 -0
  14. package/build/resources/docs.d.ts +2 -0
  15. package/build/resources/docs.js +76 -0
  16. package/build/resources/projects.d.ts +3 -0
  17. package/build/resources/projects.js +60 -0
  18. package/build/tools/find-component-usages.d.ts +21 -0
  19. package/build/tools/find-component-usages.js +216 -0
  20. package/build/tools/find-page-navigations.d.ts +26 -0
  21. package/build/tools/find-page-navigations.js +220 -0
  22. package/build/tools/get-api-endpoints.d.ts +2 -0
  23. package/build/tools/get-api-endpoints.js +126 -0
  24. package/build/tools/get-app-settings.d.ts +2 -0
  25. package/build/tools/get-app-settings.js +169 -0
  26. package/build/tools/get-app-state.d.ts +2 -0
  27. package/build/tools/get-app-state.js +96 -0
  28. package/build/tools/get-component-summary.d.ts +22 -0
  29. package/build/tools/get-component-summary.js +195 -0
  30. package/build/tools/get-custom-code.d.ts +2 -0
  31. package/build/tools/get-custom-code.js +380 -0
  32. package/build/tools/get-data-models.d.ts +2 -0
  33. package/build/tools/get-data-models.js +266 -0
  34. package/build/tools/get-editing-guide.d.ts +7 -0
  35. package/build/tools/get-editing-guide.js +185 -0
  36. package/build/tools/get-general-settings.d.ts +2 -0
  37. package/build/tools/get-general-settings.js +116 -0
  38. package/build/tools/get-in-app-purchases.d.ts +2 -0
  39. package/build/tools/get-in-app-purchases.js +51 -0
  40. package/build/tools/get-integrations.d.ts +2 -0
  41. package/build/tools/get-integrations.js +137 -0
  42. package/build/tools/get-page-by-name.d.ts +3 -0
  43. package/build/tools/get-page-by-name.js +56 -0
  44. package/build/tools/get-page-summary.d.ts +22 -0
  45. package/build/tools/get-page-summary.js +205 -0
  46. package/build/tools/get-project-config.d.ts +2 -0
  47. package/build/tools/get-project-config.js +216 -0
  48. package/build/tools/get-project-setup.d.ts +2 -0
  49. package/build/tools/get-project-setup.js +212 -0
  50. package/build/tools/get-theme.d.ts +2 -0
  51. package/build/tools/get-theme.js +199 -0
  52. package/build/tools/get-yaml-docs.d.ts +6 -0
  53. package/build/tools/get-yaml-docs.js +116 -0
  54. package/build/tools/get-yaml.d.ts +2 -0
  55. package/build/tools/get-yaml.js +53 -0
  56. package/build/tools/list-files.d.ts +3 -0
  57. package/build/tools/list-files.js +49 -0
  58. package/build/tools/list-pages.d.ts +25 -0
  59. package/build/tools/list-pages.js +101 -0
  60. package/build/tools/list-projects.d.ts +3 -0
  61. package/build/tools/list-projects.js +23 -0
  62. package/build/tools/search-project-files.d.ts +2 -0
  63. package/build/tools/search-project-files.js +69 -0
  64. package/build/tools/sync-project.d.ts +3 -0
  65. package/build/tools/sync-project.js +147 -0
  66. package/build/tools/update-yaml.d.ts +3 -0
  67. package/build/tools/update-yaml.js +24 -0
  68. package/build/tools/validate-yaml.d.ts +3 -0
  69. package/build/tools/validate-yaml.js +39 -0
  70. package/build/utils/batch-process.d.ts +2 -0
  71. package/build/utils/batch-process.js +10 -0
  72. package/build/utils/cache.d.ts +58 -0
  73. package/build/utils/cache.js +199 -0
  74. package/build/utils/decode-yaml.d.ts +7 -0
  75. package/build/utils/decode-yaml.js +31 -0
  76. package/build/utils/page-summary/action-summarizer.d.ts +24 -0
  77. package/build/utils/page-summary/action-summarizer.js +291 -0
  78. package/build/utils/page-summary/formatter.d.ts +13 -0
  79. package/build/utils/page-summary/formatter.js +129 -0
  80. package/build/utils/page-summary/node-extractor.d.ts +24 -0
  81. package/build/utils/page-summary/node-extractor.js +227 -0
  82. package/build/utils/page-summary/tree-walker.d.ts +6 -0
  83. package/build/utils/page-summary/tree-walker.js +55 -0
  84. package/build/utils/page-summary/types.d.ts +58 -0
  85. package/build/utils/page-summary/types.js +4 -0
  86. package/build/utils/parse-folders.d.ts +9 -0
  87. package/build/utils/parse-folders.js +29 -0
  88. package/build/utils/resolve-data-type.d.ts +2 -0
  89. package/build/utils/resolve-data-type.js +18 -0
  90. package/build/utils/topic-map.d.ts +7 -0
  91. package/build/utils/topic-map.js +122 -0
  92. package/docs/ff-yaml/00-overview.md +166 -0
  93. package/docs/ff-yaml/01-project-files.md +2309 -0
  94. package/docs/ff-yaml/02-pages.md +572 -0
  95. package/docs/ff-yaml/03-components.md +784 -0
  96. package/docs/ff-yaml/04-widgets/README.md +122 -0
  97. package/docs/ff-yaml/04-widgets/button.md +444 -0
  98. package/docs/ff-yaml/04-widgets/container.md +358 -0
  99. package/docs/ff-yaml/04-widgets/dropdown.md +579 -0
  100. package/docs/ff-yaml/04-widgets/form.md +256 -0
  101. package/docs/ff-yaml/04-widgets/image.md +276 -0
  102. package/docs/ff-yaml/04-widgets/layout.md +355 -0
  103. package/docs/ff-yaml/04-widgets/misc.md +553 -0
  104. package/docs/ff-yaml/04-widgets/text-field.md +326 -0
  105. package/docs/ff-yaml/04-widgets/text.md +302 -0
  106. package/docs/ff-yaml/05-actions.md +953 -0
  107. package/docs/ff-yaml/06-variables.md +849 -0
  108. package/docs/ff-yaml/07-data.md +591 -0
  109. package/docs/ff-yaml/08-custom-code.md +736 -0
  110. package/docs/ff-yaml/09-theming.md +638 -0
  111. package/docs/ff-yaml/10-editing-guide.md +497 -0
  112. package/docs/ff-yaml/README.md +105 -0
  113. package/package.json +59 -0
  114. package/skills/community-ff-mcp/SKILL.md +201 -0
  115. package/skills/ff-widget-patterns.md +141 -0
  116. package/skills/ff-yaml-dev.md +70 -0
@@ -0,0 +1,147 @@
1
+ import { z } from "zod";
2
+ import { decodeProjectYamlResponse } from "../utils/decode-yaml.js";
3
+ import { cacheWriteBulk, cacheWriteMeta, cacheWrite, cacheMeta, cacheClear, } from "../utils/cache.js";
4
+ import { fetchOneFile } from "./list-pages.js";
5
+ /**
6
+ * Extract top-level file keys from the listPartitionedFileNames response.
7
+ * Filters out deeply nested sub-files (widget nodes, etc.) that can't be
8
+ * fetched individually and would hammer the API with thousands of requests.
9
+ * Top-level keys have at most 2 path segments (e.g. "page/id-Scaffold_xxx").
10
+ */
11
+ function extractTopLevelFileKeys(fileNamesRaw) {
12
+ const raw = fileNamesRaw;
13
+ const allKeys = raw?.value?.file_names ?? raw?.value?.fileNames ?? [];
14
+ return allKeys.filter((key) => key.split("/").length <= 2);
15
+ }
16
+ /**
17
+ * Process items in batches to avoid API rate limits.
18
+ */
19
+ async function batchProcess(items, batchSize, fn) {
20
+ const results = [];
21
+ for (let i = 0; i < items.length; i += batchSize) {
22
+ const batch = items.slice(i, i + batchSize);
23
+ const batchResults = await Promise.allSettled(batch.map(fn));
24
+ results.push(...batchResults);
25
+ }
26
+ return results;
27
+ }
28
+ export function registerSyncProjectTool(server, client) {
29
+ server.tool("sync_project", "Sync an entire FlutterFlow project to the local cache. Downloads all YAML files (bulk or batched fallback) for fast offline reads. Use force=true to re-sync an already cached project.", {
30
+ projectId: z.string().describe("The FlutterFlow project ID"),
31
+ force: z
32
+ .boolean()
33
+ .optional()
34
+ .describe("Force re-sync even if cache already exists (default: false)"),
35
+ }, async ({ projectId, force }) => {
36
+ // Check existing cache unless force is set
37
+ if (!force) {
38
+ const existing = await cacheMeta(projectId);
39
+ if (existing) {
40
+ return {
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: JSON.stringify({
45
+ status: "already_cached",
46
+ message: "Project is already cached. Pass force=true to re-sync.",
47
+ cachedAt: existing.lastSyncedAt,
48
+ fileCount: existing.fileCount,
49
+ syncMethod: existing.syncMethod,
50
+ }, null, 2),
51
+ },
52
+ ],
53
+ };
54
+ }
55
+ }
56
+ // Clear stale cache before re-syncing so files that no longer exist
57
+ // on the server are removed instead of persisting as stale data.
58
+ await cacheClear(projectId);
59
+ // Primary path: bulk fetch entire project as one ZIP
60
+ try {
61
+ const raw = await client.getProjectYamls(projectId);
62
+ const decoded = decodeProjectYamlResponse(raw);
63
+ // ZIP entry names include .yaml extension, but cacheWrite adds it too.
64
+ // Strip .yaml from keys to avoid double extension (.yaml.yaml).
65
+ const normalized = {};
66
+ for (const [key, content] of Object.entries(decoded)) {
67
+ const cleanKey = key.endsWith(".yaml") ? key.slice(0, -".yaml".length) : key;
68
+ normalized[cleanKey] = content;
69
+ }
70
+ const syncedFiles = await cacheWriteBulk(projectId, normalized);
71
+ const meta = {
72
+ lastSyncedAt: new Date().toISOString(),
73
+ fileCount: syncedFiles,
74
+ syncMethod: "bulk",
75
+ };
76
+ await cacheWriteMeta(projectId, meta);
77
+ return {
78
+ content: [
79
+ {
80
+ type: "text",
81
+ text: JSON.stringify({
82
+ status: "synced",
83
+ syncedFiles,
84
+ failed: 0,
85
+ method: "bulk",
86
+ cachedAt: meta.lastSyncedAt,
87
+ }, null, 2),
88
+ },
89
+ ],
90
+ };
91
+ }
92
+ catch (err) {
93
+ // Bulk fetch failed — log and fall through to batched approach
94
+ console.error(`[sync_project] Bulk fetch failed, falling back to batched:`, err instanceof Error ? err.message : err);
95
+ }
96
+ // Fallback path: list all file keys, then batch-fetch 5 at a time
97
+ const fileNamesRaw = await client.listPartitionedFileNames(projectId);
98
+ const allKeys = extractTopLevelFileKeys(fileNamesRaw);
99
+ if (allKeys.length === 0) {
100
+ return {
101
+ content: [
102
+ {
103
+ type: "text",
104
+ text: JSON.stringify({
105
+ status: "error",
106
+ message: "No file keys returned by listPartitionedFileNames. " +
107
+ "Check the projectId.",
108
+ }, null, 2),
109
+ },
110
+ ],
111
+ };
112
+ }
113
+ let syncedFiles = 0;
114
+ let failed = 0;
115
+ const results = await batchProcess(allKeys, 5, (fileKey) => fetchOneFile(client, projectId, fileKey));
116
+ for (let i = 0; i < allKeys.length; i++) {
117
+ const result = results[i];
118
+ if (result.status === "fulfilled" && result.value) {
119
+ await cacheWrite(projectId, allKeys[i], result.value.content);
120
+ syncedFiles++;
121
+ }
122
+ else {
123
+ failed++;
124
+ }
125
+ }
126
+ const meta = {
127
+ lastSyncedAt: new Date().toISOString(),
128
+ fileCount: syncedFiles,
129
+ syncMethod: "batched",
130
+ };
131
+ await cacheWriteMeta(projectId, meta);
132
+ return {
133
+ content: [
134
+ {
135
+ type: "text",
136
+ text: JSON.stringify({
137
+ status: "synced",
138
+ syncedFiles,
139
+ failed,
140
+ method: "batched",
141
+ cachedAt: meta.lastSyncedAt,
142
+ }, null, 2),
143
+ },
144
+ ],
145
+ };
146
+ });
147
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { FlutterFlowClient } from "../api/flutterflow.js";
3
+ export declare function registerUpdateYamlTool(server: McpServer, client: FlutterFlowClient): void;
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { cacheWrite } from "../utils/cache.js";
3
+ export function registerUpdateYamlTool(server, client) {
4
+ server.tool("update_project_yaml", "Push YAML changes to a FlutterFlow project. IMPORTANT: Always call validate_yaml first to check for errors before updating. For best results, call get_editing_guide before writing YAML to get the correct workflow and schema documentation.", {
5
+ projectId: z.string().describe("The FlutterFlow project ID"),
6
+ fileKeyToContent: z
7
+ .record(z.string(), z.string())
8
+ .describe("Map of file keys to YAML content. Pass each value as a normal multi-line YAML string."),
9
+ }, async ({ projectId, fileKeyToContent }) => {
10
+ const result = await client.updateProjectByYaml(projectId, fileKeyToContent);
11
+ // Update cache with the pushed content
12
+ for (const [fileKey, content] of Object.entries(fileKeyToContent)) {
13
+ await cacheWrite(projectId, fileKey, content);
14
+ }
15
+ return {
16
+ content: [
17
+ {
18
+ type: "text",
19
+ text: JSON.stringify(result, null, 2),
20
+ },
21
+ ],
22
+ };
23
+ });
24
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { FlutterFlowClient } from "../api/flutterflow.js";
3
+ export declare function registerValidateYamlTool(server: McpServer, client: FlutterFlowClient): void;
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ /** Extract widget type from a node-level fileKey, e.g. "Button" from "node/id-Button_xyz" */
3
+ function extractWidgetTypeFromFileKey(fileKey) {
4
+ const match = fileKey.match(/node\/id-([A-Z][a-zA-Z]+)_/);
5
+ return match ? match[1] : null;
6
+ }
7
+ export function registerValidateYamlTool(server, client) {
8
+ server.tool("validate_yaml", "Validate YAML content before pushing changes to a FlutterFlow project. Always call this before update_project_yaml. Tip: Call get_editing_guide or get_yaml_docs BEFORE writing YAML to understand the correct schema and field names. Validation catches syntax errors but not semantic mistakes.", {
9
+ projectId: z.string().describe("The FlutterFlow project ID"),
10
+ fileKey: z
11
+ .string()
12
+ .describe("The YAML file key (e.g. 'app-details', 'page/id-xxx')"),
13
+ fileContent: z
14
+ .string()
15
+ .describe("Pass YAML content as a normal multi-line string."),
16
+ }, async ({ projectId, fileKey, fileContent }) => {
17
+ const result = await client.validateProjectYaml(projectId, fileKey, fileContent);
18
+ let text = JSON.stringify(result, null, 2);
19
+ // Add doc hint on validation failure
20
+ const isFailure = result && typeof result === "object" && result.valid === false;
21
+ if (isFailure) {
22
+ const widgetType = extractWidgetTypeFromFileKey(fileKey);
23
+ if (widgetType) {
24
+ text += `\n\nHint: Use get_yaml_docs(topic: "${widgetType}") to look up the correct field schema for ${widgetType} widgets.`;
25
+ }
26
+ else {
27
+ text += `\n\nHint: Use get_yaml_docs to look up the correct YAML schema for the file you're editing.`;
28
+ }
29
+ }
30
+ return {
31
+ content: [
32
+ {
33
+ type: "text",
34
+ text,
35
+ },
36
+ ],
37
+ };
38
+ });
39
+ }
@@ -0,0 +1,2 @@
1
+ /** Batch-process items in groups to avoid overwhelming the file system. */
2
+ export declare function batchProcess<T, R>(items: T[], batchSize: number, fn: (item: T) => Promise<R>): Promise<R[]>;
@@ -0,0 +1,10 @@
1
+ /** Batch-process items in groups to avoid overwhelming the file system. */
2
+ export async function batchProcess(items, batchSize, fn) {
3
+ const results = [];
4
+ for (let i = 0; i < items.length; i += batchSize) {
5
+ const batch = items.slice(i, i + batchSize);
6
+ const batchResults = await Promise.all(batch.map(fn));
7
+ results.push(...batchResults);
8
+ }
9
+ return results;
10
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Metadata stored alongside the cached YAML files for a project.
3
+ */
4
+ export interface CacheMeta {
5
+ lastSyncedAt: string;
6
+ fileCount: number;
7
+ syncMethod: "bulk" | "batched";
8
+ }
9
+ /**
10
+ * Resolve the cache directory for a given project.
11
+ * Returns an absolute path: `<projectRoot>/.ff-cache/<projectId>/`
12
+ */
13
+ export declare function cacheDir(projectId: string): string;
14
+ /**
15
+ * Read a cached YAML string for a given file key.
16
+ * Returns `null` if the file does not exist.
17
+ */
18
+ export declare function cacheRead(projectId: string, fileKey: string): Promise<string | null>;
19
+ /**
20
+ * Write a single YAML string to the cache, creating directories as needed.
21
+ */
22
+ export declare function cacheWrite(projectId: string, fileKey: string, content: string): Promise<void>;
23
+ /**
24
+ * Delete a single cached file. No-op if the file does not exist.
25
+ */
26
+ export declare function cacheInvalidate(projectId: string, fileKey: string): Promise<void>;
27
+ /**
28
+ * Delete the entire cache directory for a project, removing all cached files
29
+ * and metadata. No-op if the directory does not exist.
30
+ */
31
+ export declare function cacheClear(projectId: string): Promise<void>;
32
+ /**
33
+ * Write multiple YAML entries to the cache in one call.
34
+ * Returns the number of entries written.
35
+ */
36
+ export declare function cacheWriteBulk(projectId: string, entries: Record<string, string>): Promise<number>;
37
+ /**
38
+ * Read the `_meta.json` file for a project cache.
39
+ * Returns `null` if it does not exist.
40
+ */
41
+ export declare function cacheMeta(projectId: string): Promise<CacheMeta | null>;
42
+ /**
43
+ * Write (or overwrite) the `_meta.json` file for a project cache.
44
+ */
45
+ export declare function cacheWriteMeta(projectId: string, meta: CacheMeta): Promise<void>;
46
+ /**
47
+ * Walk the cache directory for a project and return all cached file keys,
48
+ * optionally filtered by a prefix (e.g. `"page/"` to list only pages).
49
+ *
50
+ * File keys are reconstructed by stripping the `.yaml` extension and making
51
+ * the path relative to the project cache dir.
52
+ */
53
+ export declare function listCachedKeys(projectId: string, prefix?: string): Promise<string[]>;
54
+ /**
55
+ * Format a human-readable footer indicating cache age.
56
+ * Appended to cache-based tool responses so the AI can judge staleness.
57
+ */
58
+ export declare function cacheAgeFooter(meta: CacheMeta): string;
@@ -0,0 +1,199 @@
1
+ import { mkdir, readFile, writeFile, unlink, readdir, rm } from "node:fs/promises";
2
+ import { join, dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ // ---------------------------------------------------------------------------
5
+ // Project root resolution
6
+ // ---------------------------------------------------------------------------
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ /** Project root: two levels up from src/utils/ */
9
+ const PROJECT_ROOT = resolve(__dirname, "..", "..");
10
+ const CACHE_DIR_NAME = ".ff-cache";
11
+ const META_FILE = "_meta.json";
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * Resolve the cache directory for a given project.
17
+ * Returns an absolute path: `<projectRoot>/.ff-cache/<projectId>/`
18
+ */
19
+ export function cacheDir(projectId) {
20
+ return join(PROJECT_ROOT, CACHE_DIR_NAME, projectId);
21
+ }
22
+ /**
23
+ * Convert a FlutterFlow file key to a cache file path.
24
+ * `page/id-Scaffold_xxx` -> `.ff-cache/{pid}/page/id-Scaffold_xxx.yaml`
25
+ */
26
+ function keyToPath(projectId, fileKey) {
27
+ return join(cacheDir(projectId), `${fileKey}.yaml`);
28
+ }
29
+ /**
30
+ * Ensure a directory exists (recursive mkdir, no-op if already there).
31
+ */
32
+ async function ensureDir(dir) {
33
+ await mkdir(dir, { recursive: true });
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // Public API
37
+ // ---------------------------------------------------------------------------
38
+ /**
39
+ * Read a cached YAML string for a given file key.
40
+ * Returns `null` if the file does not exist.
41
+ */
42
+ export async function cacheRead(projectId, fileKey) {
43
+ try {
44
+ const content = await readFile(keyToPath(projectId, fileKey), "utf-8");
45
+ return content;
46
+ }
47
+ catch (err) {
48
+ if (isNodeError(err) && err.code === "ENOENT") {
49
+ return null;
50
+ }
51
+ throw err;
52
+ }
53
+ }
54
+ /**
55
+ * Write a single YAML string to the cache, creating directories as needed.
56
+ */
57
+ export async function cacheWrite(projectId, fileKey, content) {
58
+ const filePath = keyToPath(projectId, fileKey);
59
+ await ensureDir(dirname(filePath));
60
+ await writeFile(filePath, content, "utf-8");
61
+ }
62
+ /**
63
+ * Delete a single cached file. No-op if the file does not exist.
64
+ */
65
+ export async function cacheInvalidate(projectId, fileKey) {
66
+ try {
67
+ await unlink(keyToPath(projectId, fileKey));
68
+ }
69
+ catch (err) {
70
+ if (isNodeError(err) && err.code === "ENOENT") {
71
+ return; // already gone — no-op
72
+ }
73
+ throw err;
74
+ }
75
+ }
76
+ /**
77
+ * Delete the entire cache directory for a project, removing all cached files
78
+ * and metadata. No-op if the directory does not exist.
79
+ */
80
+ export async function cacheClear(projectId) {
81
+ try {
82
+ await rm(cacheDir(projectId), { recursive: true, force: true });
83
+ }
84
+ catch (err) {
85
+ if (isNodeError(err) && err.code === "ENOENT") {
86
+ return; // already gone — no-op
87
+ }
88
+ throw err;
89
+ }
90
+ }
91
+ /**
92
+ * Write multiple YAML entries to the cache in one call.
93
+ * Returns the number of entries written.
94
+ */
95
+ export async function cacheWriteBulk(projectId, entries) {
96
+ const keys = Object.keys(entries);
97
+ await Promise.all(keys.map((key) => cacheWrite(projectId, key, entries[key])));
98
+ return keys.length;
99
+ }
100
+ /**
101
+ * Read the `_meta.json` file for a project cache.
102
+ * Returns `null` if it does not exist.
103
+ */
104
+ export async function cacheMeta(projectId) {
105
+ const metaPath = join(cacheDir(projectId), META_FILE);
106
+ try {
107
+ const raw = await readFile(metaPath, "utf-8");
108
+ return JSON.parse(raw);
109
+ }
110
+ catch (err) {
111
+ if (isNodeError(err) && err.code === "ENOENT") {
112
+ return null;
113
+ }
114
+ throw err;
115
+ }
116
+ }
117
+ /**
118
+ * Write (or overwrite) the `_meta.json` file for a project cache.
119
+ */
120
+ export async function cacheWriteMeta(projectId, meta) {
121
+ const dir = cacheDir(projectId);
122
+ await ensureDir(dir);
123
+ await writeFile(join(dir, META_FILE), JSON.stringify(meta, null, 2), "utf-8");
124
+ }
125
+ /**
126
+ * Walk the cache directory for a project and return all cached file keys,
127
+ * optionally filtered by a prefix (e.g. `"page/"` to list only pages).
128
+ *
129
+ * File keys are reconstructed by stripping the `.yaml` extension and making
130
+ * the path relative to the project cache dir.
131
+ */
132
+ export async function listCachedKeys(projectId, prefix) {
133
+ const root = cacheDir(projectId);
134
+ const keys = [];
135
+ try {
136
+ await walkDir(root, root, keys, ".yaml");
137
+ }
138
+ catch (err) {
139
+ if (isNodeError(err) && err.code === "ENOENT") {
140
+ return []; // cache dir doesn't exist yet
141
+ }
142
+ throw err;
143
+ }
144
+ const yamlKeys = keys.map((k) => k.slice(0, -".yaml".length));
145
+ if (prefix) {
146
+ return yamlKeys.filter((k) => k.startsWith(prefix));
147
+ }
148
+ return yamlKeys;
149
+ }
150
+ // ---------------------------------------------------------------------------
151
+ // Cache age footer
152
+ // ---------------------------------------------------------------------------
153
+ /**
154
+ * Format a human-readable footer indicating cache age.
155
+ * Appended to cache-based tool responses so the AI can judge staleness.
156
+ */
157
+ export function cacheAgeFooter(meta) {
158
+ const syncedAt = new Date(meta.lastSyncedAt);
159
+ const diffMs = Date.now() - syncedAt.getTime();
160
+ const diffMin = Math.floor(diffMs / 60_000);
161
+ let ago;
162
+ if (diffMin < 1)
163
+ ago = "just now";
164
+ else if (diffMin < 60)
165
+ ago = `${diffMin} min ago`;
166
+ else if (diffMin < 1440)
167
+ ago = `${Math.floor(diffMin / 60)}h ${diffMin % 60}m ago`;
168
+ else
169
+ ago = `${Math.floor(diffMin / 1440)}d ago`;
170
+ return `\n\n---\n_Synced: ${meta.lastSyncedAt} (${ago}). Call sync_project to refresh._`;
171
+ }
172
+ // ---------------------------------------------------------------------------
173
+ // Internal helpers
174
+ // ---------------------------------------------------------------------------
175
+ /**
176
+ * Recursively walk a directory, collecting relative paths of files.
177
+ */
178
+ async function walkDir(base, dir, out, ext) {
179
+ const entries = await readdir(dir, { withFileTypes: true });
180
+ for (const entry of entries) {
181
+ const full = join(dir, entry.name);
182
+ if (entry.isDirectory()) {
183
+ await walkDir(base, full, out, ext);
184
+ }
185
+ else if (entry.isFile()) {
186
+ if (ext && !entry.name.endsWith(ext))
187
+ continue;
188
+ // Relative path from cache root, using forward slashes for consistency
189
+ const rel = full.slice(base.length + 1).split("\\").join("/");
190
+ out.push(rel);
191
+ }
192
+ }
193
+ }
194
+ /**
195
+ * Type guard for Node.js system errors (which carry a `code` property).
196
+ */
197
+ function isNodeError(err) {
198
+ return err instanceof Error && "code" in err;
199
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Decode the base64-encoded ZIP returned by the FlutterFlow projectYamls API
3
+ * into a map of filename → YAML text.
4
+ *
5
+ * The API response shape is: { value: { projectYamlBytes: "<base64>" } }
6
+ */
7
+ export declare function decodeProjectYamlResponse(apiResponse: unknown): Record<string, string>;
@@ -0,0 +1,31 @@
1
+ import AdmZip from "adm-zip";
2
+ /**
3
+ * Decode the base64-encoded ZIP returned by the FlutterFlow projectYamls API
4
+ * into a map of filename → YAML text.
5
+ *
6
+ * The API response shape is: { value: { projectYamlBytes: "<base64>" } }
7
+ */
8
+ export function decodeProjectYamlResponse(apiResponse) {
9
+ const resp = apiResponse;
10
+ // API docs use camelCase (projectYamlBytes), but handle snake_case fallback
11
+ const b64 = resp?.value?.projectYamlBytes ?? resp?.value?.project_yaml_bytes;
12
+ if (!b64) {
13
+ throw new Error("Unexpected API response: missing value.projectYamlBytes");
14
+ }
15
+ const zipBuffer = Buffer.from(b64, "base64");
16
+ const zip = new AdmZip(zipBuffer);
17
+ const entries = zip.getEntries();
18
+ const result = {};
19
+ for (const entry of entries) {
20
+ if (!entry.isDirectory) {
21
+ try {
22
+ result[entry.entryName] = entry.getData().toString("utf-8");
23
+ }
24
+ catch {
25
+ // Skip entries that fail to decompress (e.g. buffer overflow on large pages)
26
+ console.error(`[decode-yaml] Failed to decompress entry: ${entry.entryName}`);
27
+ }
28
+ }
29
+ }
30
+ return result;
31
+ }
@@ -0,0 +1,24 @@
1
+ import { ActionSummary, TriggerSummary } from "./types.js";
2
+ /**
3
+ * Collect all action keys referenced in a trigger chain.
4
+ * Flattens followUpAction chains, conditionActions, and parallelActions.
5
+ */
6
+ export declare function collectActionKeys(node: Record<string, unknown>): string[];
7
+ /**
8
+ * Recursively search an object tree for the first recognizable action.
9
+ * Used to unwrap disableAction nodes where the real action is buried
10
+ * inside conditionalActions or other nesting.
11
+ */
12
+ export declare function findDeepAction(obj: unknown, depth?: number): ActionSummary | null;
13
+ /**
14
+ * Classify an action YAML into a human-readable summary.
15
+ */
16
+ export declare function classifyAction(doc: Record<string, unknown>): ActionSummary;
17
+ /**
18
+ * Read all trigger_actions for a node and return summaries.
19
+ *
20
+ * @param projectId - FF project ID
21
+ * @param nodeFileKeyPrefix - Cache prefix for the node's trigger actions,
22
+ * e.g. "page/id-Scaffold_xxx/page-widget-tree-outline/node/id-Button_yyy"
23
+ */
24
+ export declare function summarizeTriggers(projectId: string, nodeFileKeyPrefix: string): Promise<TriggerSummary[]>;