sparkecoder 0.1.99 → 0.1.102

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 (138) hide show
  1. package/README.md +41 -41
  2. package/dist/agent/index.js +17 -1
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/cli.js +224 -44
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.js +17 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/server/index.js +17 -1
  9. package/dist/server/index.js.map +1 -1
  10. package/dist/tools/index.d.ts +1 -1
  11. package/package.json +16 -15
  12. package/web/.next/BUILD_ID +1 -1
  13. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  14. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  15. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  16. package/web/.next/standalone/web/.next/server/app/(main)/agents/page_client-reference-manifest.js +1 -1
  17. package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
  18. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js.nft.json +1 -1
  19. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
  20. package/web/.next/standalone/web/.next/server/app/(main)/settings/page_client-reference-manifest.js +1 -1
  21. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  22. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
  31. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  34. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  38. package/web/.next/standalone/web/.next/server/app/agents.rsc +4 -4
  39. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +2 -2
  40. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +2 -2
  42. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +4 -4
  43. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +2 -2
  45. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  48. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +3 -3
  49. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +3 -3
  50. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +2 -2
  52. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +2 -2
  54. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +2 -2
  56. package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  59. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +3 -3
  60. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +3 -3
  61. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +2 -2
  63. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  66. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +2 -2
  67. package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  69. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +3 -3
  70. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +3 -3
  71. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +2 -2
  73. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +2 -2
  75. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +2 -2
  77. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  78. package/web/.next/standalone/web/.next/server/app/docs.rsc +3 -3
  79. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +3 -3
  80. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  81. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +2 -2
  82. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  83. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +2 -2
  84. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +2 -2
  85. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  86. package/web/.next/standalone/web/.next/server/app/index.rsc +4 -4
  87. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
  88. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
  89. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
  90. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  91. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  92. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  93. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  94. package/web/.next/standalone/web/.next/server/app/settings.rsc +4 -4
  95. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +2 -2
  96. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  97. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +2 -2
  98. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +4 -4
  99. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  100. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  101. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  102. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_b57914d2._.js → 2374f_1f3f2d00._.js} +1 -1
  103. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_3b04c7b5._.js → 2374f_38945fd9._.js} +1 -1
  104. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_e6dbbf5d._.js → 2374f_570c34dc._.js} +1 -1
  105. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_db1d6704._.js → 2374f_9c560f3a._.js} +1 -1
  106. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_9e444fb0._.js → 2374f_a0d5caeb._.js} +1 -1
  107. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_7b7dd4c7._.js → 2374f_c87abaf4._.js} +1 -1
  108. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_5ee1ee50._.js → 2374f_d8122230._.js} +1 -1
  109. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__6097da17._.js +1 -1
  110. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__be5e2967._.js +1 -1
  111. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_c2c47039._.js → web_3b9a2423._.js} +2 -2
  112. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  113. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  114. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  115. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  116. package/web/.next/standalone/web/.next/static/chunks/4239395558fab3ef.js +1 -0
  117. package/web/.next/standalone/web/.next/static/chunks/{f50a66c24c564585.js → ae4bb24474ff1ed0.js} +1 -1
  118. package/web/.next/standalone/web/.next/static/chunks/{545725e4c1237026.js → fbdcbd65f9a38f4b.js} +1 -1
  119. package/web/.next/standalone/web/.next/static/static/chunks/4239395558fab3ef.js +1 -0
  120. package/web/.next/standalone/web/.next/static/static/chunks/{f50a66c24c564585.js → ae4bb24474ff1ed0.js} +1 -1
  121. package/web/.next/standalone/web/.next/static/static/chunks/{545725e4c1237026.js → fbdcbd65f9a38f4b.js} +1 -1
  122. package/web/.next/standalone/web/package-lock.json +7 -7
  123. package/web/.next/standalone/web/src/components/ai-elements/mention-input.tsx +125 -17
  124. package/web/.next/static/chunks/4239395558fab3ef.js +1 -0
  125. package/web/.next/static/chunks/{f50a66c24c564585.js → ae4bb24474ff1ed0.js} +1 -1
  126. package/web/.next/static/chunks/{545725e4c1237026.js → fbdcbd65f9a38f4b.js} +1 -1
  127. package/web/.next/standalone/web/.next/static/chunks/9fce2ce79c4c834e.js +0 -1
  128. package/web/.next/standalone/web/.next/static/static/chunks/9fce2ce79c4c834e.js +0 -1
  129. package/web/.next/static/chunks/9fce2ce79c4c834e.js +0 -1
  130. /package/web/.next/standalone/web/.next/static/static/{yyzMTo2RYusiXE0e1_kdc → wV0cf4IbCdyxPIPIWZqMD}/_buildManifest.js +0 -0
  131. /package/web/.next/standalone/web/.next/static/static/{yyzMTo2RYusiXE0e1_kdc → wV0cf4IbCdyxPIPIWZqMD}/_clientMiddlewareManifest.json +0 -0
  132. /package/web/.next/standalone/web/.next/static/static/{yyzMTo2RYusiXE0e1_kdc → wV0cf4IbCdyxPIPIWZqMD}/_ssgManifest.js +0 -0
  133. /package/web/.next/standalone/web/.next/static/{yyzMTo2RYusiXE0e1_kdc → wV0cf4IbCdyxPIPIWZqMD}/_buildManifest.js +0 -0
  134. /package/web/.next/standalone/web/.next/static/{yyzMTo2RYusiXE0e1_kdc → wV0cf4IbCdyxPIPIWZqMD}/_clientMiddlewareManifest.json +0 -0
  135. /package/web/.next/standalone/web/.next/static/{yyzMTo2RYusiXE0e1_kdc → wV0cf4IbCdyxPIPIWZqMD}/_ssgManifest.js +0 -0
  136. /package/web/.next/static/{yyzMTo2RYusiXE0e1_kdc → wV0cf4IbCdyxPIPIWZqMD}/_buildManifest.js +0 -0
  137. /package/web/.next/static/{yyzMTo2RYusiXE0e1_kdc → wV0cf4IbCdyxPIPIWZqMD}/_clientMiddlewareManifest.json +0 -0
  138. /package/web/.next/static/{yyzMTo2RYusiXE0e1_kdc → wV0cf4IbCdyxPIPIWZqMD}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -8234,7 +8234,7 @@ You ALSO have the regular agent toolset (\`bash\`, \`read_file\`, \`write_file\`
8234
8234
 
8235
8235
  You manage your own configuration by editing files. Load the relevant skill first to get the file path and schema:
8236
8236
 
8237
- - **MCP integrations** \u2014 load the \`manage-mcp\` skill. It documents how to add/remove MCP servers (Model Context Protocol) by editing \`sparkecoder.config.json\`. Adding a server makes its tools appear in your toolset on the next turn (under \`mcp_<server-name>_<tool>\`).
8237
+ - **MCP integrations** \u2014 load the \`manage-mcp\` skill. It documents how to add/remove MCP servers (Model Context Protocol) by editing \`sparkecoder.config.json\`. Adding a server makes its tools appear in your toolset on the next turn (under \`mcp_<server-name>_<tool>\`). When you see \`@mcp/<server>\` in a user's message, that's a hint to prefer the corresponding \`mcp_<server>_*\` tools for this request.
8238
8238
  - **Conversation history / long-term memory** \u2014 load the \`search-conversations\` skill. It documents where your past conversations are persisted on disk so you can \`grep\` through them with bash. Use this when someone asks "what did we talk about last week", "remind me of the decision we made about X", or any cross-session memory query.
8239
8239
 
8240
8240
  If the user asks "add the GitHub MCP" or "remember that I prefer Python", load the right skill first, then act on the documented file paths with bash/read_file/write_file.
@@ -8818,6 +8818,22 @@ ${summaryContent}`
8818
8818
  content
8819
8819
  };
8820
8820
  await messageQueries.create(this.sessionId, userMessage);
8821
+ try {
8822
+ const { appendTurn: appendTurn2, flattenContent: flattenContent2 } = await Promise.resolve().then(() => (init_conversation_archive(), conversation_archive_exports));
8823
+ const { sessionQueries: sessionQueries2 } = await Promise.resolve().then(() => (init_db(), db_exports));
8824
+ const session = await sessionQueries2.getById(this.sessionId).catch(() => void 0);
8825
+ const text = flattenContent2(content).trim();
8826
+ if (!text) return;
8827
+ const channelMatch = text.match(/^\[([A-Z]+)/);
8828
+ appendTurn2({
8829
+ sessionId: this.sessionId,
8830
+ sessionName: session?.name ?? void 0,
8831
+ role: "user",
8832
+ content: text,
8833
+ channel: channelMatch ? channelMatch[1].toLowerCase() : void 0
8834
+ });
8835
+ } catch {
8836
+ }
8821
8837
  }
8822
8838
  async addResponseMessages(messages) {
8823
8839
  await messageQueries.addMany(this.sessionId, messages);
@@ -16812,53 +16828,217 @@ program.command("slack-setup").description("Interactively configure Slack integr
16812
16828
  process.exit(1);
16813
16829
  }
16814
16830
  });
16815
- program.command("cloudflared-setup").description("Print a copy-paste recipe for exposing this sparkecoder via cloudflared + Cloudflare Access").option("--port <port>", "Local sparkecoder web UI port", "6969").option("--api-port <port>", "Local sparkecoder API port", "3141").option("--hostname <host>", "Public hostname you want to assign (e.g. sf-mac-1.example.com)").option("--team <team>", "Your Cloudflare Access team subdomain (e.g. studyfetch -> studyfetch.cloudflareaccess.com)").option("--allowed-emails <emails>", "Comma-separated allow-listed emails").action(async (options) => {
16831
+ program.command("cloudflared-setup").description("Auto-detect cloudflared + set up a tunnel to this sparkecoder (interactive)").option("--port <port>", "Local sparkecoder web UI port", "6969").option("--api-port <port>", "Local sparkecoder API port", "3141").option("--hostname <host>", "Public hostname you want to assign (e.g. sf-mac-1.example.com)").option("--tunnel-name <name>", "Tunnel name to reuse or create", "sparkecoder").option("--team <team>", "Your Cloudflare Access team subdomain (e.g. studyfetch -> studyfetch.cloudflareaccess.com)").option("--allowed-emails <emails>", "Comma-separated allow-listed emails").option("-y, --yes", "Skip confirmations and do everything non-interactively", false).option("--print-only", "Skip auto-setup and just print the copy/paste recipe", false).action(async (options) => {
16832
+ const { execSync: execSync2, spawnSync } = await import("child_process");
16833
+ const { homedir: homedir2 } = await import("os");
16834
+ const { copyFileSync } = await import("fs");
16816
16835
  const port = options.port || "6969";
16817
16836
  const apiPort = options.apiPort || "3141";
16818
- const hostname = options.hostname || "<your-public-hostname>";
16837
+ const tunnelName = options.tunnelName || "sparkecoder";
16819
16838
  const team = options.team || "<your-team>";
16820
16839
  const emails = (options.allowedEmails || "").split(",").map((s) => s.trim()).filter(Boolean);
16821
- console.log(chalk.bold("Cloudflared + Cloudflare Access setup\n"));
16822
- console.log(chalk.dim("1. Install cloudflared:"));
16823
- console.log(chalk.cyan(" brew install cloudflared # macOS"));
16824
- console.log(chalk.cyan(" # or download from https://github.com/cloudflare/cloudflared/releases\n"));
16825
- console.log(chalk.dim("2. Authenticate + create a named tunnel:"));
16826
- console.log(chalk.cyan(" cloudflared tunnel login"));
16827
- console.log(chalk.cyan(" cloudflared tunnel create sparkecoder"));
16828
- console.log(chalk.cyan(` cloudflared tunnel route dns sparkecoder ${hostname}
16829
- `));
16830
- console.log(chalk.dim("3. Create ~/.cloudflared/config.yml with:"));
16831
- console.log(chalk.cyan(`
16832
- tunnel: sparkecoder
16833
- credentials-file: /Users/$(whoami)/.cloudflared/<tunnel-id>.json
16834
- ingress:
16835
- - hostname: ${hostname}
16836
- service: http://localhost:${port}
16837
- - hostname: api-${hostname}
16838
- service: http://localhost:${apiPort}
16839
- - service: http_status:404
16840
- `));
16841
- console.log(chalk.dim("4. Run the tunnel (or set it up as a system service):"));
16842
- console.log(chalk.cyan(" cloudflared tunnel run sparkecoder\n"));
16843
- console.log(chalk.dim("5. In Cloudflare Zero Trust > Access > Applications:"));
16844
- console.log(chalk.dim(` - Add a self-hosted application for ${hostname}.`));
16845
- console.log(chalk.dim(' - Add an "Allow" policy with the emails you want.'));
16846
- console.log(chalk.dim(` - Note the application AUD tag.
16847
- `));
16848
- console.log(chalk.dim("6. Add an auth block to sparkecoder.config.json:"));
16849
- console.log(chalk.cyan(`
16850
- {
16851
- "auth": {
16852
- "cfAccess": {
16853
- "enabled": true,
16854
- "teamDomain": "${team}.cloudflareaccess.com",
16855
- "audTag": "<paste-aud-from-cf-dashboard>"
16856
- },
16857
- "allowedEmails": [${emails.length ? emails.map((e) => `"${e}"`).join(", ") : '"you@example.com"'}]
16858
- }
16859
- }
16860
- `));
16861
- console.log(chalk.dim("7. Slack endpoint (/api/slack/events) and /health are exempt from CF Access automatically."));
16840
+ const yes = !!options.yes;
16841
+ if (options.printOnly) {
16842
+ const hostname = options.hostname || "<your-public-hostname>";
16843
+ console.log(chalk.bold("Cloudflared + Cloudflare Access setup (manual)\n"));
16844
+ console.log(chalk.dim("1. brew install cloudflared"));
16845
+ console.log(chalk.dim("2. cloudflared tunnel login"));
16846
+ console.log(chalk.dim(`3. cloudflared tunnel create ${tunnelName}`));
16847
+ console.log(chalk.dim(`4. cloudflared tunnel route dns ${tunnelName} ${hostname}`));
16848
+ console.log(chalk.dim(`5. write ~/.cloudflared/config.yml pointing ${hostname} -> http://localhost:${port}, api-${hostname} -> http://localhost:${apiPort}`));
16849
+ console.log(chalk.dim(`6. cloudflared tunnel run ${tunnelName}`));
16850
+ console.log(chalk.dim("7. Cloudflare Zero Trust -> add a self-hosted Access app on the hostname"));
16851
+ console.log(chalk.dim("8. Paste the AUD into sparkecoder.config.json under auth.cfAccess"));
16852
+ return;
16853
+ }
16854
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
16855
+ const ask = (q, def) => new Promise((res) => {
16856
+ const prompt = def ? `${q} ${chalk.dim(`[${def}]`)} ` : `${q} `;
16857
+ rl.question(prompt, (a) => res((a || "").trim() || def || ""));
16858
+ });
16859
+ const confirm = async (q, def = true) => {
16860
+ if (yes) return true;
16861
+ const a = (await ask(`${q} ${chalk.dim(def ? "(Y/n)" : "(y/N)")}`)).toLowerCase();
16862
+ if (!a) return def;
16863
+ return a.startsWith("y");
16864
+ };
16865
+ const which = (bin) => {
16866
+ try {
16867
+ return execSync2(`command -v ${bin}`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim() || null;
16868
+ } catch {
16869
+ return null;
16870
+ }
16871
+ };
16872
+ const run = (cmd, args, opts = {}) => {
16873
+ const r = spawnSync(cmd, args, { stdio: opts.inheritIO ? "inherit" : ["ignore", "pipe", "pipe"], encoding: "utf8" });
16874
+ if (opts.check && r.status !== 0) {
16875
+ throw new Error(`${cmd} ${args.join(" ")} failed (${r.status}): ${r.stderr || r.stdout}`);
16876
+ }
16877
+ return r;
16878
+ };
16879
+ try {
16880
+ console.log(chalk.bold("Cloudflared auto-setup\n"));
16881
+ let cfPath = which("cloudflared");
16882
+ if (!cfPath) {
16883
+ console.log(chalk.yellow("cloudflared is not installed."));
16884
+ if (process.platform === "darwin" && which("brew")) {
16885
+ if (await confirm("Install it now via `brew install cloudflared`?", true)) {
16886
+ run("brew", ["install", "cloudflared"], { inheritIO: true, check: true });
16887
+ cfPath = which("cloudflared");
16888
+ }
16889
+ }
16890
+ if (!cfPath) {
16891
+ console.log(chalk.red("Install cloudflared from https://github.com/cloudflare/cloudflared/releases and re-run."));
16892
+ rl.close();
16893
+ return;
16894
+ }
16895
+ }
16896
+ const verOut = run("cloudflared", ["--version"]).stdout?.toString().split("\n")[0] || "installed";
16897
+ console.log(chalk.green("\u2713"), "cloudflared:", chalk.dim(verOut));
16898
+ const cfDir = join16(homedir2(), ".cloudflared");
16899
+ const certPath = join16(cfDir, "cert.pem");
16900
+ if (!existsSync21(certPath)) {
16901
+ console.log(chalk.yellow("No Cloudflare login cert found."));
16902
+ if (await confirm("Run `cloudflared tunnel login` now (opens a browser)?", true)) {
16903
+ run("cloudflared", ["tunnel", "login"], { inheritIO: true, check: true });
16904
+ } else {
16905
+ console.log(chalk.red("Login required to continue."));
16906
+ rl.close();
16907
+ return;
16908
+ }
16909
+ } else {
16910
+ console.log(chalk.green("\u2713"), "logged in:", chalk.dim(certPath));
16911
+ }
16912
+ let tunnels = [];
16913
+ try {
16914
+ const out = run("cloudflared", ["tunnel", "list", "--output", "json"]).stdout || "[]";
16915
+ tunnels = JSON.parse(out);
16916
+ } catch {
16917
+ }
16918
+ let tunnel = tunnels.find((t) => t.name === tunnelName);
16919
+ if (tunnel) {
16920
+ console.log(chalk.green("\u2713"), `reusing existing tunnel "${tunnelName}":`, chalk.dim(tunnel.id));
16921
+ } else {
16922
+ if (!await confirm(`No tunnel named "${tunnelName}" found. Create it?`, true)) {
16923
+ rl.close();
16924
+ return;
16925
+ }
16926
+ const r = run("cloudflared", ["tunnel", "create", tunnelName], { inheritIO: true });
16927
+ if (r.status !== 0) {
16928
+ console.log(chalk.red("Tunnel create failed."));
16929
+ rl.close();
16930
+ return;
16931
+ }
16932
+ try {
16933
+ const out = run("cloudflared", ["tunnel", "list", "--output", "json"]).stdout || "[]";
16934
+ tunnels = JSON.parse(out);
16935
+ tunnel = tunnels.find((t) => t.name === tunnelName);
16936
+ } catch {
16937
+ }
16938
+ if (!tunnel) {
16939
+ console.log(chalk.red("Could not locate freshly-created tunnel. Check `cloudflared tunnel list`."));
16940
+ rl.close();
16941
+ return;
16942
+ }
16943
+ }
16944
+ const credsFile = tunnel.credentials_file || join16(cfDir, `${tunnel.id}.json`);
16945
+ if (!existsSync21(credsFile)) {
16946
+ console.log(chalk.yellow(`Credentials file not found at ${credsFile}. The tunnel may still work via cert.pem.`));
16947
+ }
16948
+ let hostname = options.hostname;
16949
+ if (!hostname) {
16950
+ hostname = await ask("Public hostname for this sparkecoder (e.g. sf-mac-1.example.com):");
16951
+ }
16952
+ if (!hostname) {
16953
+ console.log(chalk.red("A hostname is required to route DNS."));
16954
+ rl.close();
16955
+ return;
16956
+ }
16957
+ const dnsR = run("cloudflared", ["tunnel", "route", "dns", tunnelName, hostname]);
16958
+ if (dnsR.status === 0) {
16959
+ console.log(chalk.green("\u2713"), `DNS routed: ${hostname} -> ${tunnelName}`);
16960
+ } else {
16961
+ const err = (dnsR.stderr || dnsR.stdout || "").toString();
16962
+ if (/already exists|with the same/i.test(err)) {
16963
+ console.log(chalk.green("\u2713"), `DNS already routed for ${hostname}`);
16964
+ } else {
16965
+ console.log(chalk.yellow("DNS route warning:"), err.trim().split("\n").slice(-2).join(" "));
16966
+ }
16967
+ }
16968
+ const configPath = join16(cfDir, "config.yml");
16969
+ const configBody = `tunnel: ${tunnel.id}
16970
+ credentials-file: ${credsFile}
16971
+ ingress:
16972
+ - hostname: ${hostname}
16973
+ service: http://localhost:${port}
16974
+ - hostname: api-${hostname}
16975
+ service: http://localhost:${apiPort}
16976
+ - service: http_status:404
16977
+ `;
16978
+ let wroteConfig = false;
16979
+ if (existsSync21(configPath)) {
16980
+ const existing = readFileSync10(configPath, "utf8");
16981
+ if (existing.trim() === configBody.trim()) {
16982
+ console.log(chalk.green("\u2713"), `config.yml already up to date: ${configPath}`);
16983
+ wroteConfig = true;
16984
+ } else if (await confirm(`A different ${configPath} exists. Overwrite (a backup will be saved)?`, false)) {
16985
+ copyFileSync(configPath, `${configPath}.bak.${Date.now()}`);
16986
+ writeFileSync6(configPath, configBody);
16987
+ console.log(chalk.green("\u2713"), `wrote ${configPath} (previous saved as .bak.*)`);
16988
+ wroteConfig = true;
16989
+ } else {
16990
+ console.log(chalk.dim("Skipping config write. Suggested contents:"));
16991
+ console.log(chalk.cyan(configBody));
16992
+ }
16993
+ } else {
16994
+ writeFileSync6(configPath, configBody);
16995
+ console.log(chalk.green("\u2713"), `wrote ${configPath}`);
16996
+ wroteConfig = true;
16997
+ }
16998
+ let alreadyRunning = false;
16999
+ try {
17000
+ const psOut = execSync2("ps -axo command", { stdio: ["ignore", "pipe", "ignore"] }).toString();
17001
+ alreadyRunning = /cloudflared.*tunnel.*run/.test(psOut);
17002
+ } catch {
17003
+ }
17004
+ if (alreadyRunning) {
17005
+ console.log(chalk.green("\u2713"), "cloudflared tunnel process detected \u2014 you may need to restart it to pick up config changes.");
17006
+ } else if (wroteConfig) {
17007
+ if (await confirm("Install cloudflared as a background service (`sudo cloudflared service install`)?", false)) {
17008
+ run("sudo", ["cloudflared", "service", "install"], { inheritIO: true });
17009
+ } else {
17010
+ console.log(chalk.dim("Start it manually with:"), chalk.cyan(`cloudflared tunnel run ${tunnelName}`));
17011
+ }
17012
+ }
17013
+ console.log("");
17014
+ console.log(chalk.bold("Cloudflare Access (one manual step)"));
17015
+ console.log(chalk.dim(` 1. Open https://one.dash.cloudflare.com -> Access -> Applications -> Add an application.`));
17016
+ console.log(chalk.dim(` 2. Self-hosted, application domain = ${hostname} (and api-${hostname} if you also want the API protected).`));
17017
+ console.log(
17018
+ chalk.dim(' 3. Add an "Allow" policy with emails:'),
17019
+ chalk.cyan(emails.length ? emails.join(", ") : "you@example.com")
17020
+ );
17021
+ console.log(chalk.dim(" 4. Copy the application AUD tag from the dashboard."));
17022
+ console.log(chalk.dim(` 5. Paste it into sparkecoder.config.json:`));
17023
+ console.log(chalk.cyan(` {
17024
+ "auth": {
17025
+ "cfAccess": {
17026
+ "enabled": true,
17027
+ "teamDomain": "${team}.cloudflareaccess.com",
17028
+ "audTag": "<paste-aud-from-cf-dashboard>"
17029
+ },
17030
+ "allowedEmails": [${emails.length ? emails.map((e) => `"${e}"`).join(", ") : '"you@example.com"'}]
17031
+ }
17032
+ }`));
17033
+ console.log(chalk.dim(" (/api/slack/events, /api/inbox/* and /health are exempt from CF Access automatically.)"));
17034
+ console.log("");
17035
+ console.log(chalk.green("Done."), `Your sparkecoder should now be reachable at`, chalk.cyan(`https://${hostname}`));
17036
+ } catch (err) {
17037
+ console.error(chalk.red("Setup failed:"), err?.message || err);
17038
+ process.exitCode = 1;
17039
+ } finally {
17040
+ rl.close();
17041
+ }
16862
17042
  });
16863
17043
  program.command("sessions").description("List all sessions").option("-l, --limit <limit>", "Number of sessions to show", "20").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").action(async (options) => {
16864
17044
  const baseUrl = `http://${options.host}:${options.port}`;