@fieldwangai/agentflow 0.1.153 → 0.1.154

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 (192) hide show
  1. package/README.md +16 -34
  2. package/README.zh-CN.md +14 -34
  3. package/agents/agentflow-node-executor-code.md +13 -3
  4. package/agents/agentflow-node-executor-planning.md +13 -3
  5. package/agents/agentflow-node-executor-requirement.md +13 -3
  6. package/agents/agentflow-node-executor-test.md +13 -3
  7. package/agents/agentflow-node-executor-ui.md +13 -3
  8. package/agents/agentflow-node-executor.md +13 -3
  9. package/agents/en/agentflow-node-executor.md +13 -3
  10. package/agents/zh/agentflow-node-executor.md +13 -3
  11. package/bin/agentflow.mjs +3 -3
  12. package/bin/lib/admin-builtin-pipelines.mjs +3 -2
  13. package/bin/lib/agent-runners.mjs +0 -929
  14. package/bin/lib/auth.mjs +2 -1
  15. package/bin/lib/catalog-agents.mjs +2 -1
  16. package/bin/lib/catalog-flows.mjs +165 -90
  17. package/bin/lib/composer-agent.mjs +3 -793
  18. package/bin/lib/composer-skill-router.mjs +3 -323
  19. package/bin/lib/exec-buffered.mjs +32 -0
  20. package/bin/lib/flow-dsl/cli.mjs +276 -0
  21. package/bin/lib/flow-dsl/codegen.mjs +358 -0
  22. package/bin/lib/flow-dsl/defs.mjs +103 -0
  23. package/bin/lib/flow-dsl/index.mjs +77 -0
  24. package/bin/lib/flow-dsl/ir.mjs +305 -0
  25. package/bin/lib/flow-dsl/layout.mjs +115 -0
  26. package/bin/lib/flow-dsl/legacy-yaml.mjs +268 -0
  27. package/bin/lib/flow-dsl/lint.mjs +241 -0
  28. package/bin/lib/flow-dsl/packages.mjs +251 -0
  29. package/bin/lib/flow-dsl/parser.mjs +444 -0
  30. package/bin/lib/flow-import.mjs +96 -26
  31. package/bin/lib/flow-write.mjs +48 -12
  32. package/bin/lib/help.mjs +8 -28
  33. package/bin/lib/html-escape.mjs +19 -0
  34. package/bin/lib/http-util.mjs +49 -0
  35. package/bin/lib/legacy-flow-execution.mjs +37 -0
  36. package/bin/lib/locales/en.json +1 -1
  37. package/bin/lib/locales/zh.json +1 -1
  38. package/bin/lib/main.mjs +152 -286
  39. package/bin/lib/marketplace.mjs +206 -61
  40. package/bin/lib/model-config.mjs +10 -0
  41. package/bin/lib/node-package-archive.mjs +240 -0
  42. package/bin/lib/node-package-bootstrap.mjs +72 -0
  43. package/bin/lib/node-package-manifest.mjs +174 -0
  44. package/bin/lib/paths.mjs +42 -19
  45. package/bin/lib/prd-workflow-routes.mjs +2949 -0
  46. package/bin/lib/prd-workflow-server.mjs +4884 -0
  47. package/bin/lib/run-events.mjs +0 -66
  48. package/bin/lib/schedule-config.mjs +4 -4
  49. package/bin/lib/skill-runtime.mjs +35 -0
  50. package/bin/lib/startup-storage-migrations.mjs +274 -0
  51. package/bin/lib/ui-server.mjs +1087 -18122
  52. package/bin/lib/user-env.mjs +16 -0
  53. package/bin/lib/workspace-auto-layout.mjs +234 -0
  54. package/bin/lib/workspace-flow-store.mjs +478 -0
  55. package/bin/lib/workspace-graph-merge.mjs +41 -34
  56. package/bin/lib/workspace-routes.mjs +3088 -0
  57. package/bin/lib/workspace-server.mjs +6199 -0
  58. package/bin/lib/workspace-state.mjs +331 -0
  59. package/bin/lib/workspace.mjs +2 -1
  60. package/builtin/nodes/agent_subAgent.md +1 -0
  61. package/builtin/nodes/control_agent_toBool.md +2 -0
  62. package/builtin/nodes/control_anyOne.md +2 -0
  63. package/builtin/nodes/control_cancelled.md +2 -0
  64. package/builtin/nodes/control_cd_workspace.md +5 -11
  65. package/builtin/nodes/control_delay.md +2 -0
  66. package/builtin/nodes/control_end.md +2 -0
  67. package/builtin/nodes/control_if.md +1 -0
  68. package/builtin/nodes/control_interval_loop.md +2 -0
  69. package/builtin/nodes/control_load_mcp.md +24 -0
  70. package/builtin/nodes/control_load_skills.md +8 -23
  71. package/builtin/nodes/control_start.md +2 -0
  72. package/builtin/nodes/control_toBool.md +2 -0
  73. package/builtin/nodes/control_user_workspace.md +2 -0
  74. package/builtin/nodes/control_wait_until.md +2 -0
  75. package/builtin/nodes/display_ascii.md +1 -0
  76. package/builtin/nodes/display_chart.md +1 -0
  77. package/builtin/nodes/display_html.md +1 -0
  78. package/builtin/nodes/display_image.md +1 -0
  79. package/builtin/nodes/display_markdown.md +1 -0
  80. package/builtin/nodes/display_mermaid.md +1 -0
  81. package/builtin/nodes/display_react_app.md +1 -0
  82. package/builtin/nodes/display_table.md +1 -0
  83. package/builtin/nodes/provide_bool.md +1 -0
  84. package/builtin/nodes/provide_file.md +1 -0
  85. package/builtin/nodes/provide_password.md +1 -0
  86. package/builtin/nodes/provide_str.md +1 -0
  87. package/builtin/nodes/tool_display_share_link.md +1 -0
  88. package/builtin/nodes/tool_get_env.md +2 -0
  89. package/builtin/nodes/tool_git_checkout.md +1 -0
  90. package/builtin/nodes/tool_git_worktree_load.md +1 -0
  91. package/builtin/nodes/tool_git_worktree_unload.md +1 -0
  92. package/builtin/nodes/tool_gitlab_create_mr.md +1 -0
  93. package/builtin/nodes/tool_jenkins_build.md +2 -0
  94. package/builtin/nodes/tool_load_key.md +2 -0
  95. package/builtin/nodes/tool_nodejs.md +17 -19
  96. package/builtin/nodes/tool_print.md +2 -0
  97. package/builtin/nodes/tool_save_key.md +2 -0
  98. package/builtin/nodes/tool_set_run_env.md +1 -0
  99. package/builtin/nodes/tool_user_ask.md +2 -0
  100. package/builtin/nodes/tool_user_check.md +2 -0
  101. package/builtin/nodes/tool_wecom_send_app_markdown.md +1 -0
  102. package/builtin/nodes/tool_wecom_send_group_markdown.md +1 -0
  103. package/builtin/nodes/workspace_one_click_task.md +44 -0
  104. package/builtin/nodes/workspace_run.md +16 -0
  105. package/builtin/nodes/workspace_scheduled_run.md +16 -0
  106. package/builtin/pipelines/module-migrate/scripts/gate.mjs +37 -0
  107. package/builtin/pipelines/module-migrate/scripts/static-check.mjs +82 -0
  108. package/builtin/pipelines/module-migrate/workspace.flow.js +172 -0
  109. package/builtin/pipelines/module-migrate/workspace.layout.json +134 -0
  110. package/builtin/pipelines/new/scripts/lint-flow.mjs +38 -0
  111. package/builtin/pipelines/new/workspace.flow.js +91 -0
  112. package/builtin/pipelines/new/workspace.layout.json +70 -0
  113. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-B9xslwI0.js → WorkflowAssistantThread-CKClwj96.js} +1 -1
  114. package/builtin/web-ui/dist/assets/index-BZ5KqLur.js +870 -0
  115. package/builtin/web-ui/dist/assets/index-CEXmmwM2.css +1 -0
  116. package/builtin/web-ui/dist/index.html +2 -2
  117. package/package.json +2 -1
  118. package/reference/flow-control-capabilities.md +77 -158
  119. package/reference/flow-layout.md +1 -1
  120. package/reference/flow-prompt-handler-check.md +2 -2
  121. package/skills/agentflow-author-flow/SKILL.md +1 -1
  122. package/skills/agentflow-cli/SKILL.md +139 -24
  123. package/skills/agentflow-cli/agents/openai.yaml +2 -2
  124. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +654 -20
  125. package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +97 -0
  126. package/skills/agentflow-flow-add-instances/SKILL.md +1 -1
  127. package/skills/agentflow-flow-dsl/SKILL.md +206 -0
  128. package/skills/agentflow-flow-dsl/agents/openai.yaml +4 -0
  129. package/skills/agentflow-flow-dsl/references/node-calls.md +39 -0
  130. package/skills/agentflow-flow-edit-node-fields/SKILL.md +1 -1
  131. package/skills/agentflow-flow-recipes/SKILL.md +7 -4
  132. package/skills/agentflow-flow-recipes/references/recipes.md +97 -43
  133. package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
  134. package/skills/agentflow-node-dsl/SKILL.md +212 -0
  135. package/skills/agentflow-node-dsl/agents/openai.yaml +4 -0
  136. package/skills/agentflow-node-reference/SKILL.md +2 -2
  137. package/skills/agentflow-node-reference/references/builtin-nodes.md +166 -115
  138. package/skills/agentflow-runtime-reference/references/runtime.md +1 -1
  139. package/skills/agentflow-workspace-ascii/SKILL.md +9 -16
  140. package/skills/agentflow-workspace-graph/SKILL.md +62 -48
  141. package/skills/agentflow-workspace-html/SKILL.md +7 -2
  142. package/skills/agentflow-workspace-image/SKILL.md +6 -2
  143. package/skills/agentflow-workspace-markdown/SKILL.md +14 -21
  144. package/skills/agentflow-workspace-mermaid/SKILL.md +8 -16
  145. package/bin/lib/api-runner.mjs +0 -387
  146. package/bin/lib/apply.mjs +0 -903
  147. package/bin/lib/composer-flow-instances.mjs +0 -68
  148. package/bin/lib/composer-flow-skeleton.mjs +0 -334
  149. package/bin/lib/composer-flow-validate.mjs +0 -47
  150. package/bin/lib/composer-model-router.mjs +0 -185
  151. package/bin/lib/composer-node-schema.mjs +0 -303
  152. package/bin/lib/composer-planner.mjs +0 -751
  153. package/bin/lib/composer-script-ops.mjs +0 -233
  154. package/bin/lib/flow-static-preview.mjs +0 -104
  155. package/bin/lib/hub-login.mjs +0 -54
  156. package/bin/lib/hub-publish.mjs +0 -159
  157. package/bin/lib/hub-remote.mjs +0 -189
  158. package/bin/lib/hub.mjs +0 -299
  159. package/bin/lib/jenkins.mjs +0 -380
  160. package/bin/lib/node-execute.mjs +0 -539
  161. package/bin/lib/normalize-node-tool-command.mjs +0 -97
  162. package/bin/lib/runtime-context.mjs +0 -243
  163. package/bin/lib/scheduler.mjs +0 -601
  164. package/bin/lib/ui-print.mjs +0 -94
  165. package/bin/pipeline/build-node-prompt.mjs +0 -271
  166. package/bin/pipeline/check-cache.mjs +0 -191
  167. package/bin/pipeline/check-flow.mjs +0 -543
  168. package/bin/pipeline/collect-nodes.mjs +0 -212
  169. package/bin/pipeline/compute-cache-md5.mjs +0 -177
  170. package/bin/pipeline/ensure-run-dir.mjs +0 -71
  171. package/bin/pipeline/gc.mjs +0 -129
  172. package/bin/pipeline/get-env.mjs +0 -59
  173. package/bin/pipeline/get-resolved-values.mjs +0 -344
  174. package/bin/pipeline/load-key.mjs +0 -62
  175. package/bin/pipeline/parse-flow.mjs +0 -708
  176. package/bin/pipeline/post-process-control-if.mjs +0 -23
  177. package/bin/pipeline/post-process-node.mjs +0 -490
  178. package/bin/pipeline/pre-process-node.mjs +0 -1430
  179. package/bin/pipeline/resolve-inputs.mjs +0 -201
  180. package/bin/pipeline/run-tool-nodejs.mjs +0 -167
  181. package/bin/pipeline/save-key.mjs +0 -93
  182. package/bin/pipeline/snapshot-prior-round.mjs +0 -70
  183. package/bin/pipeline/validate-for-ui.mjs +0 -234
  184. package/bin/pipeline/validate-script-output.mjs +0 -130
  185. package/bin/pipeline/write-result.mjs +0 -182
  186. package/builtin/pipelines/module-migrate/flow.yaml +0 -819
  187. package/builtin/pipelines/new/flow.yaml +0 -545
  188. package/builtin/pipelines/new/scripts/check-flow.mjs +0 -9
  189. package/builtin/pipelines/new/scripts/collect-nodes.mjs +0 -211
  190. package/builtin/web-ui/dist/assets/index-DZ328oSo.css +0 -1
  191. package/builtin/web-ui/dist/assets/index-DmUV7ZCL.js +0 -888
  192. package/skills/agentflow-node-authoring/SKILL.md +0 -57
@@ -1,189 +0,0 @@
1
- /**
2
- * agentflow list-remote [--search <query>] [--sort popular|trending] [--json]
3
- * agentflow download <slug|title> [--as <flowId>] [--raw [--output <dir>]]
4
- */
5
- import fs from "fs";
6
- import path from "path";
7
- import chalk from "chalk";
8
- import { log } from "./log.mjs";
9
- import { Table } from "./table.mjs";
10
- import { queryFlows, queryFlowBySlug, downloadFlowFile, incrementDownload } from "./hub.mjs";
11
- import {
12
- unzipAndNormalizePipelineZip,
13
- suggestFlowIdFromZip,
14
- writePipelineTree,
15
- validateImportedFlowYaml,
16
- } from "./flow-import.mjs";
17
- import { resolveFlowDirForWrite, validateUserPipelineId } from "./flow-write.mjs";
18
-
19
- export async function hubListRemote(argv) {
20
- let search = "";
21
- let sort = "popular";
22
- const jsonMode = argv.includes("--json");
23
-
24
- const searchIdx = argv.indexOf("--search");
25
- if (searchIdx >= 0 && argv[searchIdx + 1]) search = argv[searchIdx + 1];
26
- const sortIdx = argv.indexOf("--sort");
27
- if (sortIdx >= 0 && argv[sortIdx + 1]) sort = argv[sortIdx + 1];
28
-
29
- const flows = await queryFlows({ sort, search });
30
-
31
- if (jsonMode) {
32
- process.stdout.write(JSON.stringify(flows) + "\n");
33
- return;
34
- }
35
-
36
- if (flows.length === 0) {
37
- log.info("No flows found." + (search ? " (search: " + search + ")" : ""));
38
- return;
39
- }
40
-
41
- const table = new Table({
42
- head: ["Slug", "Title", "Author", "Downloads", "Nodes", "Tags"],
43
- style: { head: [] },
44
- });
45
-
46
- for (const f of flows) {
47
- table.push([
48
- f.slug,
49
- (f.title || "").slice(0, 30),
50
- f.profiles?.username || "-",
51
- String(f.downloads || 0),
52
- String(f.node_count || 0),
53
- (f.tags || []).join(", "),
54
- ]);
55
- }
56
-
57
- log.info("\n" + chalk.bold("AgentFlow Hub — " + (sort === "trending" ? "Trending" : "Popular")) + "\n");
58
- log.info(table.toString());
59
- log.info("\n" + chalk.dim("Download: agentflow download <slug>"));
60
- }
61
-
62
- /**
63
- * Derive a filesystem-safe user pipeline id from a free-form string.
64
- * Replaces non-alnum with underscore, strips leading digits/underscores/hyphens.
65
- * Returns null if nothing usable remains.
66
- * @param {string} s
67
- * @returns {string | null}
68
- */
69
- function sanitizeToPipelineId(s) {
70
- if (!s || typeof s !== "string") return null;
71
- const cleaned = s.trim().replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^[-_]+|[-_]+$/g, "");
72
- if (!cleaned) return null;
73
- const leadDigitsStripped = cleaned.replace(/^[0-9]+/, "");
74
- const candidate = leadDigitsStripped || "flow_" + cleaned;
75
- const v = validateUserPipelineId(candidate);
76
- return v.ok ? v.flowId : null;
77
- }
78
-
79
- export async function hubDownload(argv) {
80
- const positional = argv.find((a) => !a.startsWith("--"));
81
- if (!positional) {
82
- throw new Error(
83
- "Usage: agentflow download <slug|title> [--user|--workspace] [--as <flowId>] [--raw [--output <dir>]]",
84
- );
85
- }
86
-
87
- const raw = argv.includes("--raw");
88
- let outputDir = process.cwd();
89
- const outIdx = argv.indexOf("--output");
90
- if (outIdx >= 0 && argv[outIdx + 1]) outputDir = path.resolve(argv[outIdx + 1]);
91
-
92
- let overrideId = null;
93
- const asIdx = argv.indexOf("--as");
94
- if (asIdx >= 0 && argv[asIdx + 1]) overrideId = argv[asIdx + 1];
95
-
96
- const wantWorkspace = argv.includes("--workspace");
97
- const wantUser = argv.includes("--user");
98
- if (wantWorkspace && wantUser) {
99
- throw new Error("--user and --workspace are mutually exclusive");
100
- }
101
- const flowSource = wantWorkspace ? "workspace" : "user";
102
-
103
- log.info("Looking up flow: " + chalk.bold(positional) + "...");
104
- const flow = await queryFlowBySlug(positional);
105
- if (!flow) {
106
- throw new Error("Flow not found: " + positional);
107
- }
108
-
109
- log.info("Downloading " + chalk.bold(flow.title) + " (" + flow.node_count + " nodes)...");
110
- const buffer = await downloadFlowFile(flow.yaml_key);
111
- const isZip = flow.yaml_key.endsWith(".zip");
112
- await incrementDownload(flow.slug);
113
-
114
- // ─── Raw mode: just save the artifact to outputDir. ───
115
- if (raw) {
116
- const ext = isZip ? ".zip" : ".yaml";
117
- const filename = flow.slug + ext;
118
- const outputPath = path.join(outputDir, filename);
119
- fs.mkdirSync(outputDir, { recursive: true });
120
- fs.writeFileSync(outputPath, buffer);
121
- log.info(chalk.green("✓") + " Saved: " + outputPath + " (" + (buffer.length / 1024).toFixed(1) + " KB)");
122
- if (isZip) {
123
- log.info(chalk.dim(" Unzip to use: unzip " + filename + " -d " + flow.slug));
124
- }
125
- return;
126
- }
127
-
128
- // ─── Default: install into pipelines dir (user or workspace). ───
129
- let files;
130
- if (isZip) {
131
- const un = unzipAndNormalizePipelineZip(buffer);
132
- if (!un.ok) throw new Error("Unzip failed: " + un.error);
133
- files = un.files;
134
- } else {
135
- const text = buffer.toString("utf8");
136
- const v = validateImportedFlowYaml(text);
137
- if (!v.ok) throw new Error("Invalid flow.yaml: " + v.error);
138
- files = new Map([["flow.yaml", buffer]]);
139
- }
140
-
141
- // ─── Determine target flowId. ───
142
- const candidates = [];
143
- if (overrideId) candidates.push(overrideId);
144
- if (isZip) {
145
- const s = suggestFlowIdFromZip(buffer);
146
- if (s.ok && s.suggestedFlowId) candidates.push(s.suggestedFlowId);
147
- }
148
- const fromTitle = sanitizeToPipelineId(flow.title);
149
- if (fromTitle) candidates.push(fromTitle);
150
- const fromSlug = sanitizeToPipelineId(flow.slug);
151
- if (fromSlug) candidates.push(fromSlug);
152
-
153
- let flowId = null;
154
- let lastError = null;
155
- for (const c of candidates) {
156
- const v = validateUserPipelineId(c);
157
- if (!v.ok) {
158
- lastError = v.error;
159
- continue;
160
- }
161
- flowId = v.flowId;
162
- break;
163
- }
164
- if (!flowId) {
165
- throw new Error("Could not derive a valid pipeline id" + (lastError ? ": " + lastError : ""));
166
- }
167
-
168
- const res = writePipelineTree(process.cwd(), flowId, flowSource, files);
169
- if (!res.success) {
170
- if (/已存在/.test(res.error || "")) {
171
- throw new Error(
172
- "Target pipeline already exists: " + flowId +
173
- ". Use `--as <newId>` to install under a different name, or `--raw` to just save the file.",
174
- );
175
- }
176
- throw new Error("Install failed: " + res.error);
177
- }
178
-
179
- const dirInfo = resolveFlowDirForWrite(process.cwd(), flowId, flowSource);
180
- const scopeLabel = flowSource === "workspace" ? "workspace" : "user";
181
- log.info(
182
- chalk.green("✓") + " Installed [" + scopeLabel + "]: " + chalk.bold(flowId) +
183
- " (" + (buffer.length / 1024).toFixed(1) + " KB)",
184
- );
185
- if (dirInfo.flowDir) {
186
- log.info(chalk.dim(" Path: " + dirInfo.flowDir));
187
- }
188
- log.info(chalk.dim(" Run with: agentflow apply " + flowId));
189
- }
package/bin/lib/hub.mjs DELETED
@@ -1,299 +0,0 @@
1
- /**
2
- * AgentFlow Hub — Supabase client, token persistence, and shared helpers.
3
- */
4
- import fs from "fs";
5
- import http from "http";
6
- import os from "os";
7
- import path from "path";
8
- import { spawn } from "child_process";
9
- import { log } from "./log.mjs";
10
- import { t } from "./i18n.mjs";
11
-
12
- const HUB_URL = "https://hvgnpjuiumxtymksqgki.supabase.co";
13
- const HUB_ANON_KEY = "sb_publishable_DIiFQno26UCIsN24Xpiotw_MxIwWCb8";
14
-
15
- function hubConfigPath() {
16
- return path.join(os.homedir(), ".agentflow", "hub.json");
17
- }
18
-
19
- function readHubConfig() {
20
- try {
21
- return JSON.parse(fs.readFileSync(hubConfigPath(), "utf8"));
22
- } catch {
23
- return null;
24
- }
25
- }
26
-
27
- function writeHubConfig(config) {
28
- const dir = path.dirname(hubConfigPath());
29
- fs.mkdirSync(dir, { recursive: true });
30
- fs.writeFileSync(hubConfigPath(), JSON.stringify(config, null, 2));
31
- }
32
-
33
- /**
34
- * Create a Supabase-like REST client (no dependency needed).
35
- * Uses fetch() against PostgREST and Storage endpoints.
36
- */
37
- function supabaseHeaders(accessToken) {
38
- const headers = {
39
- apikey: HUB_ANON_KEY,
40
- "Content-Type": "application/json",
41
- };
42
- if (accessToken) {
43
- headers.Authorization = "Bearer " + accessToken;
44
- } else {
45
- headers.Authorization = "Bearer " + HUB_ANON_KEY;
46
- }
47
- return headers;
48
- }
49
-
50
- // ──── Public REST helpers ────
51
-
52
- export async function queryFlows({ sort = "popular", search = "", limit = 50 } = {}) {
53
- const order = sort === "trending" ? "created_at.desc" : "downloads.desc";
54
- let url = `${HUB_URL}/rest/v1/flows?select=*,profiles!flows_author_id_fkey(username)&published=eq.true&order=${order}&limit=${limit}`;
55
- if (search) {
56
- url += `&title=ilike.*${encodeURIComponent(search)}*`;
57
- }
58
- const res = await fetch(url, { headers: supabaseHeaders() });
59
- if (!res.ok) throw new Error("Hub query failed: " + res.status);
60
- return res.json();
61
- }
62
-
63
- export async function queryFlowBySlug(slug) {
64
- const baseSelect = `select=*,profiles!flows_author_id_fkey(username)`;
65
- const bySlug = `${HUB_URL}/rest/v1/flows?${baseSelect}&slug=eq.${encodeURIComponent(slug)}&limit=1`;
66
- const slugRes = await fetch(bySlug, { headers: supabaseHeaders() });
67
- if (!slugRes.ok) throw new Error("Hub query failed: " + slugRes.status);
68
- const slugData = await slugRes.json();
69
- if (slugData[0]) return slugData[0];
70
-
71
- // Fallback: exact title match (case-insensitive) among published flows.
72
- // If multiple share the same title, pick the most downloaded.
73
- const titleEsc = encodeURIComponent(slug.replace(/[,%*()]/g, ""));
74
- const byTitle = `${HUB_URL}/rest/v1/flows?${baseSelect}&published=eq.true&title=ilike.${titleEsc}&order=downloads.desc&limit=1`;
75
- const titleRes = await fetch(byTitle, { headers: supabaseHeaders() });
76
- if (!titleRes.ok) throw new Error("Hub query failed: " + titleRes.status);
77
- const titleData = await titleRes.json();
78
- return titleData[0] || null;
79
- }
80
-
81
- export async function downloadFlowFile(yamlKey) {
82
- const url = `${HUB_URL}/storage/v1/object/public/flows/${encodeURIComponent(yamlKey)}`;
83
- const res = await fetch(url);
84
- if (!res.ok) throw new Error("Download failed: " + res.status);
85
- return Buffer.from(await res.arrayBuffer());
86
- }
87
-
88
- export async function incrementDownload(slug) {
89
- const url = `${HUB_URL}/rest/v1/rpc/increment_download`;
90
- await fetch(url, {
91
- method: "POST",
92
- headers: supabaseHeaders(),
93
- body: JSON.stringify({ flow_slug: slug }),
94
- }).catch(() => {});
95
- }
96
-
97
- // ──── Auth (login) ────
98
-
99
- export async function getStoredSession() {
100
- const config = readHubConfig();
101
- if (!config?.access_token) return null;
102
- // Try refresh if expired
103
- if (config.expires_at && Date.now() / 1000 > config.expires_at - 60) {
104
- const refreshed = await refreshSession(config.refresh_token);
105
- if (refreshed) return refreshed;
106
- return null;
107
- }
108
- return config;
109
- }
110
-
111
- async function refreshSession(refreshToken) {
112
- if (!refreshToken) return null;
113
- const url = `${HUB_URL}/auth/v1/token?grant_type=refresh_token`;
114
- const res = await fetch(url, {
115
- method: "POST",
116
- headers: { apikey: HUB_ANON_KEY, "Content-Type": "application/json" },
117
- body: JSON.stringify({ refresh_token: refreshToken }),
118
- });
119
- if (!res.ok) return null;
120
- const data = await res.json();
121
- const config = {
122
- access_token: data.access_token,
123
- refresh_token: data.refresh_token,
124
- user_id: data.user?.id,
125
- expires_at: Math.floor(Date.now() / 1000) + (data.expires_in || 3600),
126
- };
127
- writeHubConfig(config);
128
- return config;
129
- }
130
-
131
- /**
132
- * Login via OAuth: opens browser → captures token via localhost callback.
133
- */
134
- export async function loginWithBrowser(provider = "github") {
135
- return new Promise((resolve, reject) => {
136
- const server = http.createServer();
137
- server.listen(0, "127.0.0.1", () => {
138
- const port = server.address().port;
139
- const redirectTo = `http://127.0.0.1:${port}/callback`;
140
-
141
- // The callback page needs to extract hash fragment and send to server
142
- server.on("request", (req, res) => {
143
- if (req.url === "/callback" || req.url?.startsWith("/callback?")) {
144
- // Supabase returns tokens in hash fragment (#access_token=...)
145
- // Serve a page that extracts hash and POSTs it
146
- res.writeHead(200, { "Content-Type": "text/html" });
147
- res.end(`<!DOCTYPE html><html><body>
148
- <script>
149
- const h = window.location.hash.substring(1);
150
- if (h) {
151
- fetch('/token', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(Object.fromEntries(new URLSearchParams(h))) })
152
- .then(() => { document.body.innerHTML = '<h2>Login successful! You can close this tab.</h2>'; });
153
- } else {
154
- document.body.innerHTML = '<h2>Login failed. No token received.</h2>';
155
- }
156
- </script>
157
- <p>Processing login...</p></body></html>`);
158
- return;
159
- }
160
-
161
- if (req.url === "/token" && req.method === "POST") {
162
- let body = "";
163
- req.on("data", (c) => (body += c));
164
- req.on("end", () => {
165
- res.writeHead(200);
166
- res.end("ok");
167
- server.close();
168
- try {
169
- const data = JSON.parse(body);
170
- if (!data.access_token) {
171
- reject(new Error("No access_token in response"));
172
- return;
173
- }
174
- const config = {
175
- access_token: data.access_token,
176
- refresh_token: data.refresh_token || null,
177
- user_id: null,
178
- expires_at: data.expires_in
179
- ? Math.floor(Date.now() / 1000) + parseInt(data.expires_in, 10)
180
- : null,
181
- };
182
- writeHubConfig(config);
183
- resolve(config);
184
- } catch (e) {
185
- reject(e);
186
- }
187
- });
188
- return;
189
- }
190
-
191
- res.writeHead(404);
192
- res.end("Not found");
193
- });
194
-
195
- // Open browser to Supabase OAuth
196
- const authUrl =
197
- `${HUB_URL}/auth/v1/authorize?provider=${provider}&redirect_to=${encodeURIComponent(redirectTo)}`;
198
-
199
- log.info("Opening browser for " + provider + " login...");
200
- openBrowser(authUrl);
201
-
202
- // Timeout after 120 seconds
203
- setTimeout(() => {
204
- server.close();
205
- reject(new Error("Login timed out (120s). Try again."));
206
- }, 120_000);
207
- });
208
- });
209
- }
210
-
211
- function openBrowser(url) {
212
- if (process.platform === "darwin") {
213
- spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
214
- } else if (process.platform === "win32") {
215
- spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
216
- } else {
217
- spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
218
- }
219
- }
220
-
221
- // ──── Upload (publish) ────
222
-
223
- export async function uploadToStorage(accessToken, fileKey, buffer, contentType) {
224
- const url = `${HUB_URL}/storage/v1/object/flows/${fileKey}`;
225
- const res = await fetch(url, {
226
- method: "POST",
227
- headers: {
228
- apikey: HUB_ANON_KEY,
229
- Authorization: "Bearer " + accessToken,
230
- "Content-Type": contentType,
231
- "x-upsert": "true",
232
- },
233
- body: buffer,
234
- });
235
- if (!res.ok) {
236
- const text = await res.text();
237
- throw new Error("Storage upload failed: " + res.status + " " + text);
238
- }
239
- return res.json();
240
- }
241
-
242
- export async function insertFlow(accessToken, flowData) {
243
- const url = `${HUB_URL}/rest/v1/flows`;
244
- const res = await fetch(url, {
245
- method: "POST",
246
- headers: { ...supabaseHeaders(accessToken), Prefer: "return=representation" },
247
- body: JSON.stringify(flowData),
248
- });
249
- if (!res.ok) {
250
- const text = await res.text();
251
- throw new Error("Insert failed: " + res.status + " " + text);
252
- }
253
- return res.json();
254
- }
255
-
256
- export async function findFlowByAuthorAndTitle(accessToken, authorId, title) {
257
- const url = `${HUB_URL}/rest/v1/flows?select=*&author_id=eq.${encodeURIComponent(authorId)}&title=eq.${encodeURIComponent(title)}&limit=1`;
258
- const res = await fetch(url, { headers: supabaseHeaders(accessToken) });
259
- if (!res.ok) return null;
260
- const data = await res.json();
261
- return data[0] || null;
262
- }
263
-
264
- export async function updateFlow(accessToken, flowId, patch) {
265
- const url = `${HUB_URL}/rest/v1/flows?id=eq.${encodeURIComponent(flowId)}`;
266
- const res = await fetch(url, {
267
- method: "PATCH",
268
- headers: { ...supabaseHeaders(accessToken), Prefer: "return=representation" },
269
- body: JSON.stringify(patch),
270
- });
271
- if (!res.ok) {
272
- const text = await res.text();
273
- throw new Error("Update failed: " + res.status + " " + text);
274
- }
275
- return res.json();
276
- }
277
-
278
- export async function deleteStorageObject(accessToken, fileKey) {
279
- const url = `${HUB_URL}/storage/v1/object/flows/${fileKey}`;
280
- const res = await fetch(url, {
281
- method: "DELETE",
282
- headers: {
283
- apikey: HUB_ANON_KEY,
284
- Authorization: "Bearer " + accessToken,
285
- },
286
- });
287
- return res.ok;
288
- }
289
-
290
- export async function getUserProfile(accessToken) {
291
- const url = `${HUB_URL}/auth/v1/user`;
292
- const res = await fetch(url, {
293
- headers: { apikey: HUB_ANON_KEY, Authorization: "Bearer " + accessToken },
294
- });
295
- if (!res.ok) return null;
296
- return res.json();
297
- }
298
-
299
- export { HUB_URL };