<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: applescript</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/applescript.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-02-25T17:33:24+00:00</updated><author><name>Simon Willison</name></author><entry><title>Claude Code Remote Control</title><link href="https://simonwillison.net/2026/Feb/25/claude-code-remote-control/#atom-tag" rel="alternate"/><published>2026-02-25T17:33:24+00:00</published><updated>2026-02-25T17:33:24+00:00</updated><id>https://simonwillison.net/2026/Feb/25/claude-code-remote-control/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://code.claude.com/docs/en/remote-control"&gt;Claude Code Remote Control&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New Claude Code feature dropped yesterday: you can now run a "remote control" session on your computer and then use the Claude Code for web interfaces (on web, iOS and native desktop app) to send prompts to that session.&lt;/p&gt;
&lt;p&gt;It's a little bit janky right now. Initially when I tried it I got the error "Remote Control is not enabled for your account. Contact your administrator." (but I &lt;em&gt;am&lt;/em&gt; my administrator?) - then I logged out and back into the Claude Code terminal app and it started working:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude remote-control
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can only run one session on your machine at a time. If you upgrade the Claude iOS app it then shows up as "Remote Control Session (Mac)" in the Code tab.&lt;/p&gt;
&lt;p&gt;It appears not to support the &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; flag (I passed that to &lt;code&gt;claude remote-control&lt;/code&gt; and it didn't reject the option, but it also appeared to have no effect) - which means you have to approve every new action it takes.&lt;/p&gt;
&lt;p&gt;I also managed to get it to a state where every prompt I tried was met by an API 500 error.&lt;/p&gt;
&lt;p style="text-align: center;"&gt;&lt;img src="https://static.simonwillison.net/static/2026/vampire-remote.jpg" alt="Screenshot of a &amp;quot;Remote Control session&amp;quot; (Mac:dev:817b) chat interface. User message: &amp;quot;Play vampire by Olivia Rodrigo in music app&amp;quot;. Response shows an API Error: 500 {&amp;quot;type&amp;quot;:&amp;quot;error&amp;quot;,&amp;quot;error&amp;quot;:{&amp;quot;type&amp;quot;:&amp;quot;api_error&amp;quot;,&amp;quot;message&amp;quot;:&amp;quot;Internal server error&amp;quot;},&amp;quot;request_id&amp;quot;:&amp;quot;req_011CYVBLH9yt2ze2qehrX8nk&amp;quot;} with a &amp;quot;Try again&amp;quot; button. Below, the assistant responds: &amp;quot;I&amp;#39;ll play &amp;quot;Vampire&amp;quot; by Olivia Rodrigo in the Music app using AppleScript.&amp;quot; A Bash command panel is open showing an osascript command: osascript -e &amp;#39;tell application &amp;quot;Music&amp;quot; activate set searchResults to search playlist &amp;quot;Library&amp;quot; for &amp;quot;vampire Olivia Rodrigo&amp;quot; if (count of searchResults) &amp;gt; 0 then play item 1 of searchResults else return &amp;quot;Song not found in library&amp;quot; end if end tell&amp;#39;" style="max-width: 80%;" /&gt;&lt;/p&gt;

&lt;p&gt;Restarting the program on the machine also causes existing sessions to start returning mysterious API errors rather than neatly explaining that the session has terminated.&lt;/p&gt;
&lt;p&gt;I expect they'll iron out all of these issues relatively quickly. It's interesting to then contrast this to solutions like OpenClaw, where one of the big selling points is the ability to control your personal device from your phone.&lt;/p&gt;
&lt;p&gt;Claude Code still doesn't have a documented mechanism for running things on a schedule, which is the other killer feature of the Claw category of software.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: I spoke too soon: also today Anthropic announced &lt;a href="https://support.claude.com/en/articles/13854387-schedule-recurring-tasks-in-cowork"&gt;Schedule recurring tasks in Cowork&lt;/a&gt;, Claude Code's &lt;a href="https://simonwillison.net/2026/Jan/12/claude-cowork/"&gt;general agent sibling&lt;/a&gt;. These do include an important limitation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Scheduled tasks only run while your computer is awake and the Claude Desktop app is open. If your computer is asleep or the app is closed when a task is scheduled to run, Cowork will skip the task, then run it automatically once your computer wakes up or you open the desktop app again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I really hope they're working on a Cowork Cloud product.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/claudeai/status/2026418433911603668"&gt;@claudeai&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/applescript"&gt;applescript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openclaw"&gt;openclaw&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai"/><category term="generative-ai"/><category term="applescript"/><category term="llms"/><category term="anthropic"/><category term="claude"/><category term="coding-agents"/><category term="claude-code"/><category term="openclaw"/></entry><entry><title>AI-enhanced development makes me more ambitious with my projects</title><link href="https://simonwillison.net/2023/Mar/27/ai-enhanced-development/#atom-tag" rel="alternate"/><published>2023-03-27T14:38:56+00:00</published><updated>2023-03-27T14:38:56+00:00</updated><id>https://simonwillison.net/2023/Mar/27/ai-enhanced-development/#atom-tag</id><summary type="html">
    &lt;p&gt;The thing I'm most excited about in our weird new AI-enhanced reality is the way it allows me to be more &lt;em&gt;ambitious&lt;/em&gt; with my projects.&lt;/p&gt;
&lt;p&gt;As an experienced developer, ChatGPT (and GitHub Copilot) save me an enormous amount of "figuring things out" time. For everything from writing a for loop in Bash to remembering how to make a cross-domain CORS request in JavaScript - I don't need to even look things up any more, I can just prompt it and get the right answer 80% of the time.&lt;/p&gt;
&lt;p&gt;This doesn't just make me more productive: it lowers my bar for when a project is worth investing time in at all.&lt;/p&gt;
&lt;p&gt;In the past I've had plenty of ideas for projects which I've ruled out because they would take a day - or days - of work to get to a point where they're useful. I have enough other stuff to build already!&lt;/p&gt;
&lt;p&gt;But if ChatGPT can drop that down to an hour or less, those projects can suddenly become viable.&lt;/p&gt;
&lt;p&gt;Which means I'm building all sorts of weird and interesting little things that previously I wouldn't have invested the time in.&lt;/p&gt;
&lt;p&gt;I'll describe my latest one of these mini-projects in detail.&lt;/p&gt;
&lt;h4 id="archive-system"&gt;Using ChatGPT to build a system to archive ChatGPT messages&lt;/h4&gt;
&lt;p&gt;I use ChatGPT a lot, and I want my own archive of conversations I've had with it.&lt;/p&gt;
&lt;p&gt;There's no API for retrieving previous conversations at the moment, but if you open up the browser DevTools and watch the network tab you can see it loading JSON any time you navigate to an older conversation.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/chatgpt-chrome-inspector-cropped.jpg" alt="ChatGPT with the Chrome inspector open. On the left is a conversation, on the right the Chrome inspector is showing the underlying JSON that was fetched for that conversation in the Network pane." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This gave me an idea for a solution...&lt;/p&gt;
&lt;p&gt;What if I could run some JavaScript on the ChatGPT site that grabbed that JSON data and sent it off to my own server?&lt;/p&gt;
&lt;p&gt;I couldn't find the raw data in JavaScript variables available to the console, so I came up with an alternative approach: could I patch the default &lt;code&gt;fetch()&lt;/code&gt; function to intercept calls made by the ChatGPT code to that endpoint, and intercept the data that way?&lt;/p&gt;
&lt;p&gt;Normally this is where the project would end. Figuring out how to patch &lt;code&gt;fetch()&lt;/code&gt; like that is non-trivial - where I'm using the software engineer's definition of "non-trivial" as meaning "I don't know how to do that off the top of my head". I'd have to do some thinking and research! I'm already tempted to drop this idea and work on something else.&lt;/p&gt;
&lt;p&gt;But now I have ChatGPT... so I prompted it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Write a new JavaScript fetch() function which calls the old one but also saves the JSON that is retrieved - if it is used to retrieve JSON - in a window.fetchedData object&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It gave me some code, but it wasn't quite what I wanted. So I added a follow-up prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;No I need to replace the old window.fetch() function entirely - I want to patch it so that I can intercept calls made by other existing code&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And it produced the following:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-c"&gt;// Define a global object to store fetched JSON data&lt;/span&gt;
&lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;fetchedData&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// Store a reference to the original fetch function&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;originalFetch&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// Define a new fetch function that wraps the original fetch&lt;/span&gt;
&lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;fetch&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;function&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;options&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;try&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// Call the original fetch function&lt;/span&gt;
    &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;originalFetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;options&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

    &lt;span class="pl-c"&gt;// Check if the response has a JSON content type&lt;/span&gt;
    &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;contentType&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;headers&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'content-type'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;contentType&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="pl-s1"&gt;contentType&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;includes&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'application/json'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c"&gt;// If the response is JSON, clone the response so we can read it twice&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;responseClone&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;clone&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Parse the JSON data and save it to the fetchedData object&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;jsonData&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;responseClone&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;fetchedData&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;jsonData&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;

    &lt;span class="pl-c"&gt;// Return the original response&lt;/span&gt;
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;catch&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// Handle any errors that occur during the fetch&lt;/span&gt;
    &lt;span class="pl-smi"&gt;console&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'Error fetching and saving JSON:'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-k"&gt;throw&lt;/span&gt; &lt;span class="pl-s1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That's exactly what I wanted!&lt;/p&gt;
&lt;p&gt;I pasted it into the DevTools console on the ChatGPT site and clicked on some links. It worked as intended.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/8fbade64a3606645243b3438930b8159"&gt;that full ChatGPT transcript&lt;/a&gt; that got me to this point.&lt;/p&gt;
&lt;p&gt;So now I have a prototype! I've proved to myself that I can intercept the JSON data fetched by ChatGPT's own web application code.&lt;/p&gt;
&lt;p&gt;I only wanted to run my code on requests that matched &lt;code&gt;https://chat.openai.com/backend-api/conversation/...&lt;/code&gt; - I could write a regex for that, but I'd have to remember to escape the necessary characters. ChatGPT &lt;a href="https://gist.github.com/simonw/103972b7eb0c1f45d536d0a0c04cb508"&gt;did that for me&lt;/a&gt; too:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;pattern&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-pds"&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;span class="pl-cce"&gt;^&lt;/span&gt;https:&lt;span class="pl-cce"&gt;\/&lt;/span&gt;&lt;span class="pl-cce"&gt;\/&lt;/span&gt;chat&lt;span class="pl-cce"&gt;\.&lt;/span&gt;openai&lt;span class="pl-cce"&gt;\.&lt;/span&gt;com&lt;span class="pl-cce"&gt;\/&lt;/span&gt;backend-api&lt;span class="pl-cce"&gt;\/&lt;/span&gt;conversation&lt;span class="pl-cce"&gt;\/&lt;/span&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So now I have the key building blocks I need for my solution: I can intercept JSON fetches and then filter to just the data from the endpoint I care about.&lt;/p&gt;
&lt;h4 id="cors-support"&gt;But I need CORS support&lt;/h4&gt;
&lt;p&gt;My plan was to have my intercepting &lt;code&gt;fetch()&lt;/code&gt; call POST the JSON data to my own &lt;a href="https://datasette.cloud/"&gt;Datasette Cloud&lt;/a&gt; instance, similar to how I record scraped Hacker News listings as described in &lt;a href="https://simonwillison.net/2022/Dec/2/datasette-write-api/"&gt;this post about Datasette's new write API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One big problem: this means that code running on the &lt;code&gt;chat.openai.com&lt;/code&gt; domain needs to POST JSON to another server. And that means the other server needs to be serving &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"&gt;CORS headers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Datasette Cloud doesn't (yet) support CORS - and I wasn't about to implement a new production feature there just so I could solve a problem for this prototype.&lt;/p&gt;
&lt;p&gt;What I really needed was some kind of CORS proxy... a URL running somewhere which forwards traffic on to Datasette Cloud but adds CORS headers to enable it to be called from elsewhere.&lt;/p&gt;
&lt;p&gt;This represents another potential blocking point: do I really want to implement an entire proxy web application just for this little project?&lt;/p&gt;
&lt;p&gt;Here's my next ChatGPT prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Write a web app in python Starlette which has CORS enabled - including for the auth header - and forwards all incoming requests to any path to another server host specified in an environment variable&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I like &lt;a href="https://www.starlette.io/"&gt;Starlette&lt;/a&gt; and I know it has CORS support and is great for writing proxies. I was hopeful that GPT-4 had seen its documentation before the September 2021 cut-off date.&lt;/p&gt;
&lt;p&gt;ChatGPT wrote me some very solid code! It used the &lt;code&gt;python-dotenv&lt;/code&gt; package though, and I didn't feel like using that dependency. So I added:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Rewrite that to use os.environ&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The exception handling looked wrong to me, so I said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Catch httpx.HTTPError as the only exception you care about&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And after just those three prompts it produced the following:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;starlette&lt;/span&gt;.&lt;span class="pl-s1"&gt;applications&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Starlette&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;starlette&lt;/span&gt;.&lt;span class="pl-s1"&gt;middleware&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Middleware&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;starlette&lt;/span&gt;.&lt;span class="pl-s1"&gt;middleware&lt;/span&gt;.&lt;span class="pl-s1"&gt;cors&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;CORSMiddleware&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;starlette&lt;/span&gt;.&lt;span class="pl-s1"&gt;routing&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Route&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;starlette&lt;/span&gt;.&lt;span class="pl-s1"&gt;requests&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Request&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;starlette&lt;/span&gt;.&lt;span class="pl-s1"&gt;responses&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;JSONResponse&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;httpx&lt;/span&gt;

&lt;span class="pl-v"&gt;TARGET_SERVER_HOST&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;environ&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"TARGET_SERVER_HOST"&lt;/span&gt;)

&lt;span class="pl-s1"&gt;middleware&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [
    &lt;span class="pl-v"&gt;Middleware&lt;/span&gt;(
        &lt;span class="pl-v"&gt;CORSMiddleware&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;allow_origins&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;[&lt;span class="pl-s"&gt;"*"&lt;/span&gt;],
        &lt;span class="pl-s1"&gt;allow_credentials&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;allow_methods&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;[&lt;span class="pl-s"&gt;"*"&lt;/span&gt;],
        &lt;span class="pl-s1"&gt;allow_headers&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;[&lt;span class="pl-s"&gt;"*"&lt;/span&gt;],
    )
]

&lt;span class="pl-s1"&gt;app&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Starlette&lt;/span&gt;(&lt;span class="pl-s1"&gt;middleware&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;middleware&lt;/span&gt;)

&lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;forward_request&lt;/span&gt;(&lt;span class="pl-s1"&gt;request&lt;/span&gt;: &lt;span class="pl-v"&gt;Request&lt;/span&gt;):
    &lt;span class="pl-k"&gt;try&lt;/span&gt;:
        &lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-s1"&gt;httpx&lt;/span&gt;.&lt;span class="pl-v"&gt;AsyncClient&lt;/span&gt;() &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;client&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;target_url&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;f"&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-v"&gt;TARGET_SERVER_HOST&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-s1"&gt;url&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;"&lt;/span&gt;
            &lt;span class="pl-s1"&gt;method&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-s1"&gt;method&lt;/span&gt;
            &lt;span class="pl-s1"&gt;headers&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;dict&lt;/span&gt;(&lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-s1"&gt;headers&lt;/span&gt;)
            &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s"&gt;"host"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;headers&lt;/span&gt;:
                &lt;span class="pl-k"&gt;del&lt;/span&gt; &lt;span class="pl-s1"&gt;headers&lt;/span&gt;[&lt;span class="pl-s"&gt;"host"&lt;/span&gt;]
            &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-s1"&gt;method&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; [&lt;span class="pl-s"&gt;"POST"&lt;/span&gt;, &lt;span class="pl-s"&gt;"PUT"&lt;/span&gt;, &lt;span class="pl-s"&gt;"PATCH"&lt;/span&gt;]:
                &lt;span class="pl-s1"&gt;request_body&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-en"&gt;json&lt;/span&gt;()
            &lt;span class="pl-k"&gt;else&lt;/span&gt;:
                &lt;span class="pl-s1"&gt;request_body&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;None&lt;/span&gt;

            &lt;span class="pl-s1"&gt;response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;client&lt;/span&gt;.&lt;span class="pl-en"&gt;request&lt;/span&gt;(
                &lt;span class="pl-s1"&gt;method&lt;/span&gt;, &lt;span class="pl-s1"&gt;target_url&lt;/span&gt;, &lt;span class="pl-s1"&gt;headers&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;headers&lt;/span&gt;, &lt;span class="pl-s1"&gt;json&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;request_body&lt;/span&gt;
            )
            &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-v"&gt;JSONResponse&lt;/span&gt;(&lt;span class="pl-s1"&gt;response&lt;/span&gt;.&lt;span class="pl-en"&gt;json&lt;/span&gt;(), &lt;span class="pl-s1"&gt;status_code&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;response&lt;/span&gt;.&lt;span class="pl-s1"&gt;status_code&lt;/span&gt;)
    &lt;span class="pl-k"&gt;except&lt;/span&gt; &lt;span class="pl-s1"&gt;httpx&lt;/span&gt;.&lt;span class="pl-v"&gt;HTTPError&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;e&lt;/span&gt;:
        &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-v"&gt;JSONResponse&lt;/span&gt;({&lt;span class="pl-s"&gt;"error"&lt;/span&gt;: &lt;span class="pl-en"&gt;str&lt;/span&gt;(&lt;span class="pl-s1"&gt;e&lt;/span&gt;)}, &lt;span class="pl-s1"&gt;status_code&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;500&lt;/span&gt;)

&lt;span class="pl-s1"&gt;app&lt;/span&gt;.&lt;span class="pl-en"&gt;add_route&lt;/span&gt;(&lt;span class="pl-s"&gt;"/{path:path}"&lt;/span&gt;, &lt;span class="pl-s1"&gt;forward_request&lt;/span&gt;, &lt;span class="pl-s1"&gt;methods&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;[&lt;span class="pl-s"&gt;"GET"&lt;/span&gt;, &lt;span class="pl-s"&gt;"POST"&lt;/span&gt;, &lt;span class="pl-s"&gt;"PUT"&lt;/span&gt;, &lt;span class="pl-s"&gt;"PATCH"&lt;/span&gt;, &lt;span class="pl-s"&gt;"DELETE"&lt;/span&gt;])

&lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;__name__&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s"&gt;"__main__"&lt;/span&gt;:
    &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;uvicorn&lt;/span&gt;

    &lt;span class="pl-s1"&gt;uvicorn&lt;/span&gt;.&lt;span class="pl-en"&gt;run&lt;/span&gt;(&lt;span class="pl-s1"&gt;app&lt;/span&gt;, &lt;span class="pl-s1"&gt;host&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"0.0.0.0"&lt;/span&gt;, &lt;span class="pl-s1"&gt;port&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;8000&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;I tested this locally... and spotted a bug. It would fail with a 500 error if the incoming request included a &lt;code&gt;content-length&lt;/code&gt; header. I fixed that myself by adding this code:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s"&gt;"content-length"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;headers&lt;/span&gt;:
    &lt;span class="pl-k"&gt;del&lt;/span&gt; &lt;span class="pl-s1"&gt;headers&lt;/span&gt;[&lt;span class="pl-s"&gt;"content-length"&lt;/span&gt;]&lt;/pre&gt;
&lt;p&gt;My &lt;a href="https://gist.github.com/simonw/e0a5368d8b465febefe6d4ae6f47f2da"&gt;finished code is here&lt;/a&gt;. Here's &lt;a href="https://gist.github.com/simonw/d95809e5a5c1441281cd5585270ab834"&gt;the ChatGPT transcript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I deployed this to Vercel using the method &lt;a href="https://til.simonwillison.net/zeit-now/python-asgi-on-now-v2"&gt;described in this TIL&lt;/a&gt; - and now I had a working proxy server.&lt;/p&gt;
&lt;h4 id="creating-tables"&gt;Creating the tables and a token&lt;/h4&gt;
&lt;p&gt;ChatGPT had got me a long way. The rest of my implementation was now a small enough lift that I could quickly finish it by myself.&lt;/p&gt;
&lt;p&gt;I created two tables in my Datasette Cloud instance by executing the following SQL (using the &lt;a href="https://datasette.io/plugins/datasette-write"&gt;datasette-write&lt;/a&gt; plugin):&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;create&lt;/span&gt; &lt;span class="pl-k"&gt;table&lt;/span&gt; &lt;span class="pl-en"&gt;chatgpt_conversation&lt;/span&gt; (
  id &lt;span class="pl-k"&gt;text&lt;/span&gt; &lt;span class="pl-k"&gt;primary key&lt;/span&gt;,
  title &lt;span class="pl-k"&gt;text&lt;/span&gt;,
  create_time float,
  moderation_results &lt;span class="pl-k"&gt;text&lt;/span&gt;,
  current_node &lt;span class="pl-k"&gt;text&lt;/span&gt;,
  plugin_ids &lt;span class="pl-k"&gt;text&lt;/span&gt;
);
&lt;span class="pl-k"&gt;create&lt;/span&gt; &lt;span class="pl-k"&gt;table&lt;/span&gt; &lt;span class="pl-en"&gt;chatgpt_message&lt;/span&gt; (
  id &lt;span class="pl-k"&gt;text&lt;/span&gt; &lt;span class="pl-k"&gt;primary key&lt;/span&gt;,
  conversation_id &lt;span class="pl-k"&gt;text&lt;/span&gt; &lt;span class="pl-k"&gt;references&lt;/span&gt; chatgpt_conversation(id),
  author_role &lt;span class="pl-k"&gt;text&lt;/span&gt;,
  author_metadata &lt;span class="pl-k"&gt;text&lt;/span&gt;,
  create_time float,
  content &lt;span class="pl-k"&gt;text&lt;/span&gt;,
  end_turn &lt;span class="pl-k"&gt;integer&lt;/span&gt;,
  weight float,
  metadata &lt;span class="pl-k"&gt;text&lt;/span&gt;,
  recipient &lt;span class="pl-k"&gt;text&lt;/span&gt;
);&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then I made myself a Datasette API token with permission to &lt;code&gt;insert-row&lt;/code&gt; and &lt;code&gt;update-row&lt;/code&gt; just for those two tables, using the new &lt;a href="https://simonwillison.net/2022/Dec/15/datasette-1a2/#finely-grained-permissions"&gt;finely grained permissions feature&lt;/a&gt; in the 1.0 alpha series.&lt;/p&gt;
&lt;p&gt;The last step was to combine this all together into a &lt;code&gt;fetch()&lt;/code&gt; function that did the right thing. I wrote this code by hand, using the ChatGPT prototype as a starting point:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;TOKEN&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;"dstok_my-token-here"&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// Store a reference to the original fetch function&lt;/span&gt;
&lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;originalFetch&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// Define a new fetch function that wraps the original fetch&lt;/span&gt;

&lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;fetch&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;options&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;try&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// Call the original fetch function&lt;/span&gt;
    &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;originalFetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;options&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

    &lt;span class="pl-c"&gt;// Check if the response has a JSON content type&lt;/span&gt;
    &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;contentType&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;headers&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"content-type"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;contentType&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="pl-s1"&gt;contentType&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;includes&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"application/json"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c"&gt;// If the response is JSON, clone the response so we can read it twice&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;responseClone&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;clone&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Parse the JSON data and save it to the fetchedData object&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;jsonData&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;responseClone&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-c"&gt;// NOW: if url for https://chat.openai.com/backend-api/conversation/...&lt;/span&gt;
      &lt;span class="pl-c"&gt;// do something very special with it&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;pattern&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt;
        &lt;span class="pl-pds"&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;span class="pl-cce"&gt;^&lt;/span&gt;https:&lt;span class="pl-cce"&gt;\/&lt;/span&gt;&lt;span class="pl-cce"&gt;\/&lt;/span&gt;chat&lt;span class="pl-cce"&gt;\.&lt;/span&gt;openai&lt;span class="pl-cce"&gt;\.&lt;/span&gt;com&lt;span class="pl-cce"&gt;\/&lt;/span&gt;backend-api&lt;span class="pl-cce"&gt;\/&lt;/span&gt;conversation&lt;span class="pl-cce"&gt;\/&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;.&lt;span class="pl-c1"&gt;*&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;match&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;match&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;pattern&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;match&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;conversationId&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;match&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;1&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-smi"&gt;console&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;log&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"conversationId"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;conversationId&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-smi"&gt;console&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;log&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"jsonData"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;jsonData&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;conversation&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
          &lt;span class="pl-c1"&gt;id&lt;/span&gt;: &lt;span class="pl-s1"&gt;conversationId&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-c1"&gt;title&lt;/span&gt;: &lt;span class="pl-s1"&gt;jsonData&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;title&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-c1"&gt;create_time&lt;/span&gt;: &lt;span class="pl-s1"&gt;jsonData&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;create_time&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-c1"&gt;moderation_results&lt;/span&gt;: &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;jsonData&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;moderation_results&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-c1"&gt;current_node&lt;/span&gt;: &lt;span class="pl-s1"&gt;jsonData&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;current_node&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-c1"&gt;plugin_ids&lt;/span&gt;: &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;jsonData&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;plugin_ids&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
        &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
          &lt;span class="pl-s"&gt;"https://starlette-cors-proxy-simonw-datasette.vercel.app/data/chatgpt_conversation/-/insert"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-kos"&gt;{&lt;/span&gt;
            &lt;span class="pl-c1"&gt;method&lt;/span&gt;: &lt;span class="pl-s"&gt;"POST"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;headers&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
              &lt;span class="pl-s"&gt;"Content-Type"&lt;/span&gt;: &lt;span class="pl-s"&gt;"application/json"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;Authorization&lt;/span&gt;: &lt;span class="pl-s"&gt;`Bearer &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-c1"&gt;TOKEN&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;mode&lt;/span&gt;: &lt;span class="pl-s"&gt;"cors"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;body&lt;/span&gt;: &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
              &lt;span class="pl-c1"&gt;row&lt;/span&gt;: &lt;span class="pl-s1"&gt;conversation&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;replace&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-kos"&gt;}&lt;/span&gt;
        &lt;span class="pl-kos"&gt;)&lt;/span&gt;
          &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
          &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-smi"&gt;console&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;log&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"d"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;messages&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Object&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;values&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;jsonData&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;mapping&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
          &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;filter&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
          &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
            &lt;span class="pl-s1"&gt;m&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
            &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;content&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;""&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
            &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
              &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
                &lt;span class="pl-s1"&gt;content&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
              &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;else&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
                &lt;span class="pl-s1"&gt;content&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;parts&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;join&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"\n"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
              &lt;span class="pl-kos"&gt;}&lt;/span&gt;
            &lt;span class="pl-kos"&gt;}&lt;/span&gt;
            &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
              &lt;span class="pl-c1"&gt;id&lt;/span&gt;: &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;id&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;conversation_id&lt;/span&gt;: &lt;span class="pl-s1"&gt;conversationId&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;author_role&lt;/span&gt;: &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;author&lt;/span&gt; ? &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;author&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;role&lt;/span&gt; : &lt;span class="pl-c1"&gt;null&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;author_metadata&lt;/span&gt;: &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
                &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;author&lt;/span&gt; ? &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;author&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;metadata&lt;/span&gt; : &lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;
              &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;create_time&lt;/span&gt;: &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;create_time&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-s1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;end_turn&lt;/span&gt;: &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;end_turn&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;weight&lt;/span&gt;: &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;weight&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;metadata&lt;/span&gt;: &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;metadata&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;recipient&lt;/span&gt;: &lt;span class="pl-s1"&gt;m&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;recipient&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
          &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
          &lt;span class="pl-s"&gt;"https://starlette-cors-proxy-simonw-datasette.vercel.app/data/chatgpt_message/-/insert"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-kos"&gt;{&lt;/span&gt;
            &lt;span class="pl-c1"&gt;method&lt;/span&gt;: &lt;span class="pl-s"&gt;"POST"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;headers&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
              &lt;span class="pl-s"&gt;"Content-Type"&lt;/span&gt;: &lt;span class="pl-s"&gt;"application/json"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;Authorization&lt;/span&gt;: &lt;span class="pl-s"&gt;`Bearer &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-c1"&gt;TOKEN&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;mode&lt;/span&gt;: &lt;span class="pl-s"&gt;"cors"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;body&lt;/span&gt;: &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
              &lt;span class="pl-c1"&gt;rows&lt;/span&gt;: &lt;span class="pl-s1"&gt;messages&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
              &lt;span class="pl-c1"&gt;replace&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-kos"&gt;}&lt;/span&gt;
        &lt;span class="pl-kos"&gt;)&lt;/span&gt;
          &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
          &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-smi"&gt;console&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;log&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"d"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;

    &lt;span class="pl-c"&gt;// Return the original response&lt;/span&gt;
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;catch&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// Handle any errors that occur during the fetch&lt;/span&gt;
    &lt;span class="pl-smi"&gt;console&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"Error fetching and saving JSON:"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-k"&gt;throw&lt;/span&gt; &lt;span class="pl-s1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The fiddly bit here was writing the JavaScript that reshaped the ChatGPT JSON into the &lt;code&gt;rows: [array-of-objects]&lt;/code&gt; format needed by the &lt;a href="https://docs.datasette.io/en/1.0a2/json_api.html#the-json-write-api"&gt;Datasette JSON APIs&lt;/a&gt;. I could probably have gotten ChatGPT to help with that - but in this case I pasted the SQL schema into a comment and let GitHub Copilot auto-complete parts of the JavaScript for me as I typed it.&lt;/p&gt;
&lt;h4 id="and-it-works"&gt;And it works&lt;/h4&gt;
&lt;p&gt;Now I can paste the above block of code into the browser console on &lt;code&gt;chat.openai.com&lt;/code&gt; and any time I click on one of my older conversations in the sidebar the &lt;code&gt;fetch()&lt;/code&gt; will be intercepted and the JSON data will be saved to my Datasette Cloud instance.&lt;/p&gt;
&lt;!-- &lt;h4 id="public-demo"&gt;A public demo&lt;/h4&gt;
&lt;p&gt;I've set up a public demo exposing messages from selected conversations here:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://simon.datasette.cloud/data/chatgpt_public_messages?_facet=conversation"&gt;simon.datasette.cloud/data/chatgpt_public_messages&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The demo itself is powered by an extra table (listing the conversations that should be public) and a SQL view.&lt;/p&gt;
&lt;p&gt;I used the &lt;code&gt;datasette-write&lt;/code&gt; plugin again to create these:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;create&lt;/span&gt; &lt;span class="pl-k"&gt;table&lt;/span&gt; &lt;span class="pl-en"&gt;chatgpt_public&lt;/span&gt; (id &lt;span class="pl-k"&gt;text&lt;/span&gt; &lt;span class="pl-k"&gt;primary key&lt;/span&gt;);

&lt;span class="pl-k"&gt;create&lt;/span&gt; &lt;span class="pl-k"&gt;view&lt;/span&gt; &lt;span class="pl-en"&gt;chatgpt_public_messages&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-k"&gt;select&lt;/span&gt;
  &lt;span class="pl-c1"&gt;chatgpt_message&lt;/span&gt;.&lt;span class="pl-c1"&gt;id&lt;/span&gt;,
  &lt;span class="pl-c1"&gt;chatgpt_conversation&lt;/span&gt;.&lt;span class="pl-c1"&gt;title&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-k"&gt;char&lt;/span&gt;(&lt;span class="pl-c1"&gt;10&lt;/span&gt;) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-c1"&gt;chatgpt_conversation&lt;/span&gt;.&lt;span class="pl-c1"&gt;id&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; conversation,
  &lt;span class="pl-c1"&gt;chatgpt_message&lt;/span&gt;.&lt;span class="pl-c1"&gt;author_role&lt;/span&gt;,
  &lt;span class="pl-c1"&gt;chatgpt_message&lt;/span&gt;.&lt;span class="pl-c1"&gt;content&lt;/span&gt;,
  datetime(&lt;span class="pl-c1"&gt;chatgpt_message&lt;/span&gt;.&lt;span class="pl-c1"&gt;create_time&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;unixepoch&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; create_time
&lt;span class="pl-k"&gt;from&lt;/span&gt;
  chatgpt_message &lt;span class="pl-k"&gt;join&lt;/span&gt; chatgpt_conversation &lt;span class="pl-k"&gt;on&lt;/span&gt; conversation_id &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;chatgpt_conversation&lt;/span&gt;.&lt;span class="pl-c1"&gt;id&lt;/span&gt;
&lt;span class="pl-k"&gt;where&lt;/span&gt;
  &lt;span class="pl-c1"&gt;chatgpt_message&lt;/span&gt;.&lt;span class="pl-c1"&gt;create_time&lt;/span&gt; &lt;span class="pl-k"&gt;is not null&lt;/span&gt;
  &lt;span class="pl-k"&gt;and&lt;/span&gt; conversation_id &lt;span class="pl-k"&gt;in&lt;/span&gt; (&lt;span class="pl-k"&gt;select&lt;/span&gt; id &lt;span class="pl-k"&gt;from&lt;/span&gt; chatgpt_public)
&lt;span class="pl-k"&gt;order by&lt;/span&gt;
  &lt;span class="pl-c1"&gt;chatgpt_message&lt;/span&gt;.&lt;span class="pl-c1"&gt;create_time&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then I set the &lt;code&gt;chatgpt_public_messages&lt;/code&gt; view to be public (using &lt;a href="https://datasette.io/plugins/datasette-public"&gt;datasette-public&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Now I can insert conversation IDs into that &lt;code&gt;chatgpt_public&lt;/code&gt; table to expose their messages in the public view.&lt;/p&gt;
&lt;p&gt;This is the first time I've used a SQL view like this to selectively publish data from a private larger table, and I think it's a really neat pattern. I'd like to make it easier to do without writing custom SQL though!&lt;/p&gt; --&gt;
&lt;h4 id="more-than-just-this"&gt;It's a lot more than just this project&lt;/h4&gt;
&lt;p&gt;This ChatGPT archiving problem is just one example from the past few months of things I've built that I wouldn't have tackled without AI-assistance.&lt;/p&gt;
&lt;p&gt;It took me longer to write this up than it did to implement the entire project from start to finish!&lt;/p&gt;
&lt;p&gt;When evaluating if a new technology is worth learning and adopting, I have two criteria:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Does this let me build things that would have been impossible to build without it?&lt;/li&gt;
&lt;li&gt;Can this reduce the effort required for some projects such that they tip over from "not worth it" to "worth it" and I end up building them?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Large language models like GPT3/4/LLaMA/Claude etc clearly meet both of those criteria - and their impact on point two keeps on getting stronger for me.&lt;/p&gt;
&lt;h4 id="some-more-examples"&gt;Some more examples&lt;/h4&gt;
&lt;p&gt;Here are a few more examples of projects I've worked on recently that wouldn't have happened without at least some level of AI assistance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I used ChatGPT to &lt;a href="https://simonwillison.net/2023/Mar/24/datasette-chatgpt-plugin/"&gt;generate me the OpenAI schema&lt;/a&gt; I needed to build the &lt;a href="https://datasette.io/plugins/datasette-chatgpt-plugin"&gt;datasette-chatgpt-plugin&lt;/a&gt; plugin, allowing human language questions in ChatGPT to be answered by SQL queries executed against Datasette.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/gpt3/chatgpt-applescript"&gt;Using ChatGPT to write AppleScript&lt;/a&gt; describes how I used ChatGPT to finally figure out enough AppleScript to liberate my notes data, resulting in building &lt;a href="https://datasette.io/tools/apple-notes-to-sqlite"&gt;apple-notes-to-sqlite&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;datasette-paste-table&lt;/code&gt; isn't in a usable state yet, but I built the first interactive prototype for that &lt;a href="https://github.com/simonw/datasette-paste-table/issues/1"&gt;using ChatGPT&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/jq/git-log-json"&gt;Convert git log output to JSON using jq&lt;/a&gt; is something I figured out using ChatGPT - &lt;a href="https://gist.github.com/simonw/c3b486fa90d7c32a0e8dfb47e151090a"&gt;transcript here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2022/Dec/5/rust-chatgpt-copilot/"&gt;Learning Rust with ChatGPT, Copilot and Advent of Code&lt;/a&gt; describes one of my earlier efforts to use ChatGPT to help learn a completely new (to me) programming language.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/googlecloud/video-frame-ocr"&gt;Reading thermometer temperatures over time from a video&lt;/a&gt; describes a project I built using ffmpg and Google Cloud Vision.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/datasette/row-selection-prototype"&gt;Interactive row selection prototype with Datasette&lt;/a&gt; explains a more complex HTML and JavaScript UI prototype I worked on.&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chatgpt"&gt;chatgpt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-copilot"&gt;github-copilot&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/applescript"&gt;applescript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/starlette"&gt;starlette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="ai"/><category term="generative-ai"/><category term="chatgpt"/><category term="github-copilot"/><category term="applescript"/><category term="llms"/><category term="ai-assisted-programming"/><category term="starlette"/></entry><entry><title>apple-notes-to-sqlite</title><link href="https://simonwillison.net/2023/Mar/9/apple-notes-to-sqlite/#atom-tag" rel="alternate"/><published>2023-03-09T06:04:28+00:00</published><updated>2023-03-09T06:04:28+00:00</updated><id>https://simonwillison.net/2023/Mar/9/apple-notes-to-sqlite/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/dogsheep/apple-notes-to-sqlite"&gt;apple-notes-to-sqlite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
With the help of ChatGPT I finally figured out just enough AppleScript to automate the export of my notes to a SQLite database. AppleScript is a notoriously read-only language, which is turns out makes it a killer app for LLM-assisted coding.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://til.simonwillison.net/gpt3/chatgpt-applescript"&gt;Using ChatGPT to write AppleScript&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apple"&gt;apple&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dogsheep"&gt;dogsheep&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chatgpt"&gt;chatgpt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/applescript"&gt;applescript&lt;/a&gt;&lt;/p&gt;



</summary><category term="apple"/><category term="projects"/><category term="sqlite"/><category term="ai"/><category term="dogsheep"/><category term="generative-ai"/><category term="chatgpt"/><category term="applescript"/></entry><entry><title>Using ChatGPT to write AppleScript</title><link href="https://simonwillison.net/2023/Mar/9/chatgpt-applescript/#atom-tag" rel="alternate"/><published>2023-03-09T05:41:07+00:00</published><updated>2023-03-09T05:41:07+00:00</updated><id>https://simonwillison.net/2023/Mar/9/chatgpt-applescript/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;TIL:&lt;/strong&gt; &lt;a href="https://til.simonwillison.net/gpt3/chatgpt-applescript"&gt;Using ChatGPT to write AppleScript&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/applescript"&gt;applescript&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="applescript"/></entry><entry><title>AppScripting</title><link href="https://simonwillison.net/2004/Jan/6/appscripting/#atom-tag" rel="alternate"/><published>2004-01-06T02:12:48+00:00</published><updated>2004-01-06T02:12:48+00:00</updated><id>https://simonwillison.net/2004/Jan/6/appscripting/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://freespace.virgin.net/hamish.sanderson/appscripting.html"&gt;AppScripting&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The power of AppleScript with the less verbose syntax of Python

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://www.decafbad.com/blog/links/boodjoajod.html"&gt;0xDECAFBAD&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/applescript"&gt;applescript&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="applescript"/></entry><entry><title>Targets for 2004</title><link href="https://simonwillison.net/2004/Jan/1/targets2004/#atom-tag" rel="alternate"/><published>2004-01-01T22:55:03+00:00</published><updated>2004-01-01T22:55:03+00:00</updated><id>https://simonwillison.net/2004/Jan/1/targets2004/#atom-tag</id><summary type="html">
    &lt;p&gt;Looking back on 2003, one thing really stuck out for me: I didn't learn a new programming language. &lt;a href="http://www.pragmaticprogrammer.com/"&gt;The Pragmatic Programmers&lt;/a&gt; recommend learning at least one new language every year, and I've heard similar advice from other notable authorities; Ruby's Matz made a similar point in &lt;a href="http://www.artima.com/intv/craft.html" title="Matz on Craftsmanship"&gt;an article&lt;/a&gt; published just the other day.&lt;/p&gt;

&lt;p&gt;I learned a number of new skills and technologies in 2003, and improved on my existing knowledge in more ways than I can count. Unfortunately, everything was building on stuff I already had experience with: I didn't really learn anything completely new.&lt;/p&gt;

&lt;p&gt;2004 will be very different, mainly because I will finally have access to a Mac. Objective C and AppleScript are both on my list, and I'm looking forward to doing some serious messing around with &lt;acronym title="Graphical User Interface"&gt;GUI&lt;/acronym&gt;s as well.&lt;/p&gt;

&lt;p&gt;Other than that, my two main geeky goals for this year are to become more active in the open source world (hopefully contributing some useful code) and to do a lot more writing outside of my blog. On a personal note, I plan to see at least some of America outside of Kansas. I'm only here until October and I want to make the most of it.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/objective-c"&gt;objective-c&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/applescript"&gt;applescript&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="objective-c"/><category term="applescript"/></entry><entry><title>X vs. XP: Scriptability</title><link href="https://simonwillison.net/2003/Dec/30/x/#atom-tag" rel="alternate"/><published>2003-12-30T19:08:29+00:00</published><updated>2003-12-30T19:08:29+00:00</updated><id>https://simonwillison.net/2003/Dec/30/x/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.xvsxp.com/scriptability/"&gt;X vs. XP: Scriptability&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I can’t wait to get my teeth in to AppleScript

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://www.apple.com/applescript/news/"&gt;Apple - Software - AppleScript - News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/applescript"&gt;applescript&lt;/a&gt;&lt;/p&gt;



</summary><category term="applescript"/></entry><entry><title>Blogging with AppleScript</title><link href="https://simonwillison.net/2003/Jan/16/appleScriptBlogging/#atom-tag" rel="alternate"/><published>2003-01-16T22:29:17+00:00</published><updated>2003-01-16T22:29:17+00:00</updated><id>https://simonwillison.net/2003/Jan/16/appleScriptBlogging/#atom-tag</id><summary type="html">
    &lt;p&gt;Les Orchard &lt;a href="http://www.decafbad.com/news_archives/000383.phtml" title="Folder Actions + AppleScript = Desktop Blogging"&gt;describes&lt;/a&gt; an intriguing blogging tool built with AppleScript that posts links to a weblog when they are dragged on to a special folder on the OS X desktop.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/les-orchard"&gt;les-orchard&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/applescript"&gt;applescript&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="les-orchard"/><category term="applescript"/></entry></feed>