//worknotesFeed.belt -- build the news.rss.chat feed from the server and client worknotes -- 7/24/26 by CC //Each bold-led entry in the worknotes becomes an item; the category element says which side it came from. //The feed is written next to the docs sources, so it rides the same build that publishes the worknotes. const theSources = [ {category: "server", path: "/Users/davewiner/Claude/rssNetwork/docs/rss.chat/server/code/worknotes.md", urlPage: "https://github.com/scripting/rss.chat/blob/main/server/code/worknotes.md"}, {category: "client", path: "/Users/davewiner/Claude/rssNetwork/docs/rss.chat/client/code/worknotes.md", urlPage: "https://github.com/scripting/rss.chat/blob/main/client/code/worknotes.md"} ]; const urlFeedHome = "https://news.rss.chat/"; const urlFeedSelf = "https://news.rss.chat/worknotes/rss.xml"; const pathFeedFile = "/Users/davewiner/Claude/rssNetwork/misc/worknotesRss.xml"; //a build artifact, deliberately OUTSIDE docs/ -- the docs tree mirrors the repo, and the one place to subscribe is news.rss.chat function parseWorknotes (filetext, theSource) { //returns an array of items, one per bold-led entry const theLines = filetext.split ("\n"); const theItems = []; var currentWhen, currentItem; theLines.forEach (function (theLine) { if (theLine.indexOf ("####") === 0) { //a dated heading -- "#### 7/23/26; 7:30 PM ET by CC" var dateText = theLine.replace ("####", "").trim (); const ixBy = dateText.indexOf (" by "); if (ixBy > 0) { dateText = dateText.substring (0, ixBy); } const theParts = dateText.split (";"); var parseText = theParts [0].trim (); //the date always comes first if ((theParts [1] !== undefined) && (/\d:\d\d/.test (theParts [1]))) { //take the second field only when it is a time -- the oldest entries carry a version number there ("4/14/26; v0.4.7"), which parsed as an Invalid Date and silently froze the sort (NaN compares as equal) parseText += " " + theParts [1].replace (" ET", "").trim (); } currentWhen = new Date (parseText); //this machine runs Eastern time, same as the timestamps currentItem = undefined; return; } if (theLine.indexOf ("**") === 0) { //a bold lead starts a new entry const ixEndBold = theLine.indexOf ("**", 2); var titleText = theLine.substring (2, ixEndBold).trim (); if (titleText.endsWith (".")) { titleText = titleText.substring (0, titleText.length - 1); } currentItem = { title: titleText, when: currentWhen, bodyText: theLine.substring (ixEndBold + 2).trim (), category: theSource.category, urlPage: theSource.urlPage }; theItems.push (currentItem); return; } if (theLine.trim ().length > 0) { if (currentItem === undefined) { //an entry with no bold lead -- older format; titles are optional here, the item just has no title currentItem = { title: undefined, when: currentWhen, bodyText: theLine.trim (), category: theSource.category, urlPage: theSource.urlPage }; theItems.push (currentItem); } else { //follow-on lines belong to the entry above currentItem.bodyText += "\n\n" + theLine; } } }); return (theItems); } function resolveRelativeLinks (htmltext, urlPage) { //worknotes carry repo-relative links -- point them at the github page's neighborhood return htmltext.replace (/href="([^"]+)"/g, function (wholeMatch, theHref) { if ((theHref.indexOf ("http://") === 0) || (theHref.indexOf ("https://") === 0) || (theHref.indexOf ("mailto:") === 0) || (theHref.indexOf ("#") === 0)) { return (wholeMatch); } const baseParts = urlPage.split ("/"); baseParts.pop (); //drop the page's own filename var resolved = theHref; while (resolved.indexOf ("../") === 0) { //each ../ climbs one folder resolved = resolved.substring (3); baseParts.pop (); } return ('href="' + baseParts.join ("/") + "/" + resolved + '"'); }); } function getGuid (theItem) { //stable across regenerations -- derived only from the entry itself const slugSource = (theItem.title !== undefined) ? theItem.title : theItem.bodyText; var slug = slugSource.toLowerCase ().replace (/[^a-z0-9]+/g, "-"); if (slug.length > 60) { slug = slug.substring (0, 60); } return (urlFeedHome + theItem.category + "/" + theItem.when.getTime () + "/" + slug); } var allItems = []; for (var i = 0; i < theSources.length; i++) { const theSource = theSources [i]; const filetext = file.readWholeFile (theSource.path); allItems = allItems.concat (parseWorknotes (filetext, theSource)); } allItems.sort (function (itemA, itemB) { return (itemB.when - itemA.when); //newest first }); const feedItems = []; for (var i = 0; i < allItems.length; i++) { const theItem = allItems [i]; var htmltext = plugins.markdown.process (theItem.bodyText); htmltext = resolveRelativeLinks (htmltext, theItem.urlPage); feedItems.push ({ title: theItem.title, text: htmltext, when: theItem.when, link: theItem.urlPage, guid: {flPermalink: false, value: getGuid (theItem)}, categories: [theItem.category] }); } const headElements = { title: "rss.chat worknotes", link: urlFeedHome, description: "Every improvement to rss.chat as it ships -- the worknotes from the server and the client, as a feed.", language: "en-us", generator: "worknotesFeed.belt", maxFeedItems: 100, urlSelf: urlFeedSelf, account: {service: "rss.chat", name: "claude"}, //the author -- find me there flRssCloudEnabled: true, //readers who speak rssCloud hear about updates in seconds rssCloudDomain: "rpc.rsscloud.io", rssCloudPort: 5337, rssCloudPath: "/pleaseNotify", rssCloudRegisterProcedure: "", rssCloudProtocol: "http-post" }; const xmltext = rss.buildRssFeed (headElements, feedItems); file.sureFilePath (pathFeedFile); file.writeWholeFile (pathFeedFile, xmltext); sys.runUnixCommand ("scp -i ~/.ssh/claude_droplet " + pathFeedFile + " root@104.248.52.44:/root/marin/pagepark/domains/news.rss.chat/worknotes/rss.xml"); //publish to news.rss.chat, which lives on marin sys.runUnixCommand ("scp -i ~/.ssh/claude_droplet /Users/davewiner/Claude/rssNetwork/belterScripts/worknotesFeed.belt root@104.248.52.44:/root/marin/pagepark/domains/news.rss.chat/worknotes/worknotesFeed.belt"); //the script publishes itself, so the current source is always next to the feed it made http.post ("http://rpc.rsscloud.io:5337/ping", "url=" + encodeURIComponent (urlFeedSelf), {"Content-Type": "application/x-www-form-urlencoded"}); //tell the cloud the feed updated var ctServer = 0, ctClient = 0; allItems.forEach (function (theItem) { if (theItem.category === "server") { ctServer++; } else { ctClient++; } }); console.log ("worknotesFeed: " + feedItems.length + " items (" + ctServer + " server, " + ctClient + " client) -> " + pathFeedFile);