@fieldwangai/agentflow 0.1.153 → 0.1.156

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 (193) 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 +277 -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 +303 -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 +1092 -18123
  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-preview.mjs +12 -2
  57. package/bin/lib/workspace-routes.mjs +3088 -0
  58. package/bin/lib/workspace-server.mjs +6199 -0
  59. package/bin/lib/workspace-state.mjs +331 -0
  60. package/bin/lib/workspace.mjs +2 -1
  61. package/builtin/nodes/agent_subAgent.md +1 -0
  62. package/builtin/nodes/control_agent_toBool.md +2 -0
  63. package/builtin/nodes/control_anyOne.md +2 -0
  64. package/builtin/nodes/control_cancelled.md +2 -0
  65. package/builtin/nodes/control_cd_workspace.md +5 -11
  66. package/builtin/nodes/control_delay.md +2 -0
  67. package/builtin/nodes/control_end.md +2 -0
  68. package/builtin/nodes/control_if.md +1 -0
  69. package/builtin/nodes/control_interval_loop.md +2 -0
  70. package/builtin/nodes/control_load_mcp.md +24 -0
  71. package/builtin/nodes/control_load_skills.md +8 -23
  72. package/builtin/nodes/control_start.md +2 -0
  73. package/builtin/nodes/control_toBool.md +2 -0
  74. package/builtin/nodes/control_user_workspace.md +2 -0
  75. package/builtin/nodes/control_wait_until.md +2 -0
  76. package/builtin/nodes/display_ascii.md +1 -0
  77. package/builtin/nodes/display_chart.md +1 -0
  78. package/builtin/nodes/display_html.md +1 -0
  79. package/builtin/nodes/display_image.md +1 -0
  80. package/builtin/nodes/display_markdown.md +1 -0
  81. package/builtin/nodes/display_mermaid.md +1 -0
  82. package/builtin/nodes/display_react_app.md +1 -0
  83. package/builtin/nodes/display_table.md +1 -0
  84. package/builtin/nodes/provide_bool.md +1 -0
  85. package/builtin/nodes/provide_file.md +1 -0
  86. package/builtin/nodes/provide_password.md +1 -0
  87. package/builtin/nodes/provide_str.md +1 -0
  88. package/builtin/nodes/tool_display_share_link.md +1 -0
  89. package/builtin/nodes/tool_get_env.md +2 -0
  90. package/builtin/nodes/tool_git_checkout.md +1 -0
  91. package/builtin/nodes/tool_git_worktree_load.md +1 -0
  92. package/builtin/nodes/tool_git_worktree_unload.md +1 -0
  93. package/builtin/nodes/tool_gitlab_create_mr.md +1 -0
  94. package/builtin/nodes/tool_jenkins_build.md +2 -0
  95. package/builtin/nodes/tool_load_key.md +2 -0
  96. package/builtin/nodes/tool_nodejs.md +17 -19
  97. package/builtin/nodes/tool_print.md +2 -0
  98. package/builtin/nodes/tool_save_key.md +2 -0
  99. package/builtin/nodes/tool_set_run_env.md +1 -0
  100. package/builtin/nodes/tool_user_ask.md +2 -0
  101. package/builtin/nodes/tool_user_check.md +2 -0
  102. package/builtin/nodes/tool_wecom_send_app_markdown.md +1 -0
  103. package/builtin/nodes/tool_wecom_send_group_markdown.md +1 -0
  104. package/builtin/nodes/workspace_one_click_task.md +44 -0
  105. package/builtin/nodes/workspace_run.md +16 -0
  106. package/builtin/nodes/workspace_scheduled_run.md +16 -0
  107. package/builtin/pipelines/module-migrate/scripts/gate.mjs +37 -0
  108. package/builtin/pipelines/module-migrate/scripts/static-check.mjs +82 -0
  109. package/builtin/pipelines/module-migrate/workspace.flow.js +172 -0
  110. package/builtin/pipelines/module-migrate/workspace.layout.json +134 -0
  111. package/builtin/pipelines/new/scripts/lint-flow.mjs +38 -0
  112. package/builtin/pipelines/new/workspace.flow.js +91 -0
  113. package/builtin/pipelines/new/workspace.layout.json +70 -0
  114. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-B9xslwI0.js → WorkflowAssistantThread-ClNxY2Wu.js} +1 -1
  115. package/builtin/web-ui/dist/assets/index-BJzMYRK3.js +870 -0
  116. package/builtin/web-ui/dist/assets/index-CEXmmwM2.css +1 -0
  117. package/builtin/web-ui/dist/index.html +2 -2
  118. package/package.json +3 -1
  119. package/reference/flow-control-capabilities.md +77 -158
  120. package/reference/flow-layout.md +1 -1
  121. package/reference/flow-prompt-handler-check.md +2 -2
  122. package/skills/agentflow-author-flow/SKILL.md +1 -1
  123. package/skills/agentflow-cli/SKILL.md +139 -24
  124. package/skills/agentflow-cli/agents/openai.yaml +2 -2
  125. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +654 -20
  126. package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +97 -0
  127. package/skills/agentflow-flow-add-instances/SKILL.md +1 -1
  128. package/skills/agentflow-flow-dsl/SKILL.md +206 -0
  129. package/skills/agentflow-flow-dsl/agents/openai.yaml +4 -0
  130. package/skills/agentflow-flow-dsl/references/node-calls.md +39 -0
  131. package/skills/agentflow-flow-edit-node-fields/SKILL.md +1 -1
  132. package/skills/agentflow-flow-recipes/SKILL.md +7 -4
  133. package/skills/agentflow-flow-recipes/references/recipes.md +97 -43
  134. package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
  135. package/skills/agentflow-node-dsl/SKILL.md +212 -0
  136. package/skills/agentflow-node-dsl/agents/openai.yaml +4 -0
  137. package/skills/agentflow-node-reference/SKILL.md +2 -2
  138. package/skills/agentflow-node-reference/references/builtin-nodes.md +166 -115
  139. package/skills/agentflow-runtime-reference/references/runtime.md +1 -1
  140. package/skills/agentflow-workspace-ascii/SKILL.md +9 -16
  141. package/skills/agentflow-workspace-graph/SKILL.md +62 -48
  142. package/skills/agentflow-workspace-html/SKILL.md +7 -2
  143. package/skills/agentflow-workspace-image/SKILL.md +6 -2
  144. package/skills/agentflow-workspace-markdown/SKILL.md +14 -21
  145. package/skills/agentflow-workspace-mermaid/SKILL.md +8 -16
  146. package/bin/lib/api-runner.mjs +0 -387
  147. package/bin/lib/apply.mjs +0 -903
  148. package/bin/lib/composer-flow-instances.mjs +0 -68
  149. package/bin/lib/composer-flow-skeleton.mjs +0 -334
  150. package/bin/lib/composer-flow-validate.mjs +0 -47
  151. package/bin/lib/composer-model-router.mjs +0 -185
  152. package/bin/lib/composer-node-schema.mjs +0 -303
  153. package/bin/lib/composer-planner.mjs +0 -751
  154. package/bin/lib/composer-script-ops.mjs +0 -233
  155. package/bin/lib/flow-static-preview.mjs +0 -104
  156. package/bin/lib/hub-login.mjs +0 -54
  157. package/bin/lib/hub-publish.mjs +0 -159
  158. package/bin/lib/hub-remote.mjs +0 -189
  159. package/bin/lib/hub.mjs +0 -299
  160. package/bin/lib/jenkins.mjs +0 -380
  161. package/bin/lib/node-execute.mjs +0 -539
  162. package/bin/lib/normalize-node-tool-command.mjs +0 -97
  163. package/bin/lib/runtime-context.mjs +0 -243
  164. package/bin/lib/scheduler.mjs +0 -601
  165. package/bin/lib/ui-print.mjs +0 -94
  166. package/bin/pipeline/build-node-prompt.mjs +0 -271
  167. package/bin/pipeline/check-cache.mjs +0 -191
  168. package/bin/pipeline/check-flow.mjs +0 -543
  169. package/bin/pipeline/collect-nodes.mjs +0 -212
  170. package/bin/pipeline/compute-cache-md5.mjs +0 -177
  171. package/bin/pipeline/ensure-run-dir.mjs +0 -71
  172. package/bin/pipeline/gc.mjs +0 -129
  173. package/bin/pipeline/get-env.mjs +0 -59
  174. package/bin/pipeline/get-resolved-values.mjs +0 -344
  175. package/bin/pipeline/load-key.mjs +0 -62
  176. package/bin/pipeline/parse-flow.mjs +0 -708
  177. package/bin/pipeline/post-process-control-if.mjs +0 -23
  178. package/bin/pipeline/post-process-node.mjs +0 -490
  179. package/bin/pipeline/pre-process-node.mjs +0 -1430
  180. package/bin/pipeline/resolve-inputs.mjs +0 -201
  181. package/bin/pipeline/run-tool-nodejs.mjs +0 -167
  182. package/bin/pipeline/save-key.mjs +0 -93
  183. package/bin/pipeline/snapshot-prior-round.mjs +0 -70
  184. package/bin/pipeline/validate-for-ui.mjs +0 -234
  185. package/bin/pipeline/validate-script-output.mjs +0 -130
  186. package/bin/pipeline/write-result.mjs +0 -182
  187. package/builtin/pipelines/module-migrate/flow.yaml +0 -819
  188. package/builtin/pipelines/new/flow.yaml +0 -545
  189. package/builtin/pipelines/new/scripts/check-flow.mjs +0 -9
  190. package/builtin/pipelines/new/scripts/collect-nodes.mjs +0 -211
  191. package/builtin/web-ui/dist/assets/index-DZ328oSo.css +0 -1
  192. package/builtin/web-ui/dist/assets/index-DmUV7ZCL.js +0 -888
  193. 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 };