diff --git a/.prettierignore b/.prettierignore index b6a27c98..ef53c5f9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ pnpm*.yaml packages/api/drizzle/* +**/*.src.md diff --git a/README.md b/README.md index 0cae582d..4d593104 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Srcbook runs locally on your machine and is fully open-source under the Apache2 Under the hood, Srcbook creates folders on your local machine and provides a web interface (also running locally) as a programming environment. -Srcbooks export to a `.srcmd` format, a superset of markdown. You can easily export Srcbooks into this format from the application, as well as import them. Given that they are a form of markdown, they are very git-friendly. +Srcbooks export to markdown using the `.src.md` extension. These files can easily be shared, versioned, and rendered in any environment that supports Markdown, like your editor or GitHub UI. To learn more, try out the interactive tutorial which is itself a Srcbook by clicking "Getting Started" when launching the application. diff --git a/packages/api/db/schema.mts b/packages/api/db/schema.mts index 29b903a2..42dc357b 100644 --- a/packages/api/db/schema.mts +++ b/packages/api/db/schema.mts @@ -2,7 +2,7 @@ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; import { randomid } from '@srcbook/shared'; export const configs = sqliteTable('config', { - // Directory where .srcmd files will be stored and searched by default. + // Directory where .src.md files will be stored and searched by default. baseDir: text('base_dir').notNull(), defaultLanguage: text('default_language').notNull().default('typescript'), openaiKey: text('openai_api_key'), diff --git a/packages/api/prompts/cell-generator-javascript.txt b/packages/api/prompts/cell-generator-javascript.txt index c1d5ab08..b4ee376b 100644 --- a/packages/api/prompts/cell-generator-javascript.txt +++ b/packages/api/prompts/cell-generator-javascript.txt @@ -2,7 +2,7 @@ You are tasked with generating Srcbook cells for the user. -A Srcbook is a JavaScript notebook following a markdown-compatible format called .srcmd. +A Srcbook is a JavaScript notebook following a markdown-compatible format called `.src.md`. ## Srcbook spec diff --git a/packages/api/prompts/cell-generator-typescript.txt b/packages/api/prompts/cell-generator-typescript.txt index 8e8be55e..913f6c98 100644 --- a/packages/api/prompts/cell-generator-typescript.txt +++ b/packages/api/prompts/cell-generator-typescript.txt @@ -2,7 +2,7 @@ You are tasked with generating Srcbook cells for the user. -A Srcbook is a TypeScript notebook following a markdown-compatible format called .srcmd. +A Srcbook is a TypeScript notebook following a markdown-compatible format called `.src.md`. ## Srcbook spec diff --git a/packages/api/prompts/srcbook-generator.txt b/packages/api/prompts/srcbook-generator.txt index d297f069..00c7e8e4 100644 --- a/packages/api/prompts/srcbook-generator.txt +++ b/packages/api/prompts/srcbook-generator.txt @@ -2,7 +2,7 @@ You are tasked with generating a srcbook about a topic, which will be requested by the user. -A srcbook is a TypeScript or JavaScript notebook following a markdown-compatible format called .srcmd. It's an interactive and rich way of programming that follows the literate programming idea. +A srcbook is a TypeScript or JavaScript notebook following a markdown-compatible format called .src.md. It's an interactive and rich way of programming that follows the literate programming idea. Srcbooks are either in javascript, or typescript, and their markdown always starts with an html comment specifying the language, for example for javascript: @@ -152,11 +152,11 @@ const token = auth(API_KEY); ## Exporting and sharing Srcbooks -Srcbooks are meant to be collaborative. They export to a friendly `.srcmd` format, which is valid markdown and can be opened in any text editor. +Srcbooks are meant to be collaborative. They export to a friendly `.src.md` format, which is valid markdown and can be opened in any text editor. You can export Srcbooks by clicking the `Export` link in the top level menu on the left. -You can also import `.srcmd` files directly in this application if you want to run, modify, or re-export them. +You can also import `.src.md` files directly in this application if you want to run, modify, or re-export them. ### Example 2: LangGraph web agent diff --git a/packages/api/server/http.mts b/packages/api/server/http.mts index d7abe966..76cdf44c 100644 --- a/packages/api/server/http.mts +++ b/packages/api/server/http.mts @@ -25,6 +25,7 @@ import { import { readdir } from '../fs-utils.mjs'; import { EXAMPLE_SRCBOOKS } from '../srcbook/examples.mjs'; import { pathToSrcbook } from '../srcbook/path.mjs'; +import { isSrcmdPath } from '../srcmd/paths.mjs'; const app: Application = express(); @@ -40,7 +41,7 @@ router.post('/disk', cors(), async (req, res) => { try { const config = await getConfig(); dirname = dirname || config.baseDir; - const entries = await disk(dirname, '.srcmd'); + const entries = await disk(dirname, '.src.md'); return res.json({ error: false, result: { dirname, entries } }); } catch (e) { const error = e as unknown as Error; @@ -87,13 +88,13 @@ router.delete('/srcbooks/:id', cors(), async (req, res) => { return res.json({ error: false, deleted: true }); }); -// Import a srcbook from a .srcmd file or srcmd text. +// Import a srcbook from a .src.md file or srcmd text. router.options('/import', cors()); router.post('/import', cors(), async (req, res) => { const { path, text } = req.body; - if (path && Path.extname(path) !== '.srcmd') { - return res.json({ error: true, result: 'Importing only works with .srcmd files' }); + if (typeof path === 'string' && !isSrcmdPath(path)) { + return res.json({ error: true, result: 'Importing only works with .src.md files' }); } try { diff --git a/packages/api/session.mts b/packages/api/session.mts index 392f4751..d8feb998 100644 --- a/packages/api/session.mts +++ b/packages/api/session.mts @@ -109,7 +109,7 @@ export async function updateSession( export async function exportSrcmdFile(session: SessionType, destinationPath: string) { if (await fileExists(destinationPath)) { - throw new Error(`Cannot export .srcmd file: ${destinationPath} already exists`); + throw new Error(`Cannot export .src.md file: ${destinationPath} already exists`); } return fs.writeFile(destinationPath, encode(session.cells, session.metadata, { inline: true })); diff --git a/packages/api/srcbook/examples.mts b/packages/api/srcbook/examples.mts index 617fd37c..c82e0bce 100644 --- a/packages/api/srcbook/examples.mts +++ b/packages/api/srcbook/examples.mts @@ -9,7 +9,7 @@ import { DIST_DIR, SRCBOOKS_DIR } from '../constants.mjs'; const GETTING_STARTED_SRCBOOK = { id: '30v2av4eee17m59dg2c29758to', - path: Path.join(DIST_DIR, 'srcbook', 'examples', 'getting-started.srcmd'), + path: Path.join(DIST_DIR, 'srcbook', 'examples', 'getting-started.src.md'), title: 'Getting started', description: 'Quick tutorial to explore the basic concepts in Srcbooks.', get dirname() { @@ -19,7 +19,7 @@ const GETTING_STARTED_SRCBOOK = { const LANGGRAPH_AGENT_SRCBOOK = { id: 'i72jjpkqepmg5olneffvk7hgto', - path: Path.join(DIST_DIR, 'srcbook', 'examples', 'langgraph-web-agent.srcmd'), + path: Path.join(DIST_DIR, 'srcbook', 'examples', 'langgraph-web-agent.src.md'), title: 'LangGraph agent', description: 'Learn to write a stateful agent with memory using LangGraph and Tavily.', get dirname() { @@ -29,7 +29,7 @@ const LANGGRAPH_AGENT_SRCBOOK = { const INTRO_TO_WEBSOCKETS_SRCBOOK = { id: 'vnovpn5dbrthpdllvoeqahufc4', - path: Path.join(DIST_DIR, 'srcbook', 'examples', 'websockets.srcmd'), + path: Path.join(DIST_DIR, 'srcbook', 'examples', 'websockets.src.md'), title: 'Intro to WebSockets', description: 'Learn to build a simple WebSocket client and server in Node.js.', get dirname() { diff --git a/packages/api/srcbook/examples/getting-started.srcmd b/packages/api/srcbook/examples/getting-started.src.md similarity index 90% rename from packages/api/srcbook/examples/getting-started.srcmd rename to packages/api/srcbook/examples/getting-started.src.md index 3cd039ed..40371d05 100644 --- a/packages/api/srcbook/examples/getting-started.srcmd +++ b/packages/api/srcbook/examples/getting-started.src.md @@ -76,8 +76,8 @@ const token = auth(API_KEY); ## Exporting and sharing Srcbooks -Srcbooks are meant to be collaborative. They export to a friendly `.srcmd` format, which is valid markdown and can be opened in any text editor. +Srcbooks are meant to be collaborative. They export to a Markdown file with the `.src.md` extension, which can be rendered in any editor or UI that supports Markdown. You can export Srcbooks by clicking the `Export` link in the top level menu on the left. -You can also import `.srcmd` files directly in this application if you want to run, modify, or re-export them. +You can also import `.src.md` files directly in this application if you want to run, modify, or re-export them. diff --git a/packages/api/srcbook/examples/langgraph-web-agent.srcmd b/packages/api/srcbook/examples/langgraph-web-agent.src.md similarity index 76% rename from packages/api/srcbook/examples/langgraph-web-agent.srcmd rename to packages/api/srcbook/examples/langgraph-web-agent.src.md index 1c9a75ef..d72457a2 100644 --- a/packages/api/srcbook/examples/langgraph-web-agent.srcmd +++ b/packages/api/srcbook/examples/langgraph-web-agent.src.md @@ -44,13 +44,13 @@ Now, let's define the Agent with LangGraph.js ###### agent.ts ```typescript -import { HumanMessage } from "@langchain/core/messages"; -import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; -import { ChatOpenAI } from "@langchain/openai"; -import { END, START, StateGraph, StateGraphArgs } from "@langchain/langgraph"; -import { SqliteSaver } from "@langchain/langgraph/checkpoint/sqlite" +import { HumanMessage } from '@langchain/core/messages'; +import { TavilySearchResults } from '@langchain/community/tools/tavily_search'; +import { ChatOpenAI } from '@langchain/openai'; +import { END, START, StateGraph, StateGraphArgs } from '@langchain/langgraph'; +import { SqliteSaver } from '@langchain/langgraph/checkpoint/sqlite'; // import { MemorySaver } from "@langchain/langgraph"; -import { ToolNode } from "@langchain/langgraph/prebuilt"; +import { ToolNode } from '@langchain/langgraph/prebuilt'; // Define the state interface interface AgentState { @@ -58,10 +58,10 @@ interface AgentState { } // We'll use a local sqlite DB for memory -export const DB_NAME = 'langgraph_memory.db' +export const DB_NAME = 'langgraph_memory.db'; // Define the graph state -const graphState: StateGraphArgs["channels"] = { +const graphState: StateGraphArgs['channels'] = { messages: { value: (x: HumanMessage[], y: HumanMessage[]) => x.concat(y), default: () => [], @@ -75,13 +75,13 @@ const toolNode = new ToolNode(tools); const model = new ChatOpenAI({ model: 'gpt-4o', temperature: 0 }).bindTools(tools); // Define the function that determines whether to continue or not -function shouldContinue(state: AgentState): "tools" | typeof END { +function shouldContinue(state: AgentState): 'tools' | typeof END { const messages = state.messages; const lastMessage = messages[messages.length - 1]; // If the LLM makes a tool call, then we route to the "tools" node if (lastMessage.additional_kwargs.tool_calls) { - return "tools"; + return 'tools'; } // Otherwise, we stop (reply to the user) return END; @@ -98,11 +98,11 @@ async function callModel(state: AgentState) { // Define a new graph const workflow = new StateGraph({ channels: graphState }) - .addNode("agent", callModel) - .addNode("tools", toolNode) - .addEdge(START, "agent") - .addConditionalEdges("agent", shouldContinue) - .addEdge("tools", "agent"); + .addNode('agent', callModel) + .addNode('tools', toolNode) + .addEdge(START, 'agent') + .addConditionalEdges('agent', shouldContinue) + .addEdge('tools', 'agent'); // Initialize memory to persist state between graph runs export const memory = SqliteSaver.fromConnString(DB_NAME); @@ -112,7 +112,6 @@ export const memory = SqliteSaver.fromConnString(DB_NAME); // This compiles it into a LangChain Runnable. // Note that we're (optionally) passing the memory when compiling the graph export const app = workflow.compile({ checkpointer: memory }); - ``` Now that we've built our app, let's invoke it to first get the weather in SF: @@ -120,19 +119,19 @@ Now that we've built our app, let's invoke it to first get the weather in SF: ###### sf-weather.ts ```typescript -import {app} from './agent.ts'; -import { HumanMessage } from "@langchain/core/messages"; +import { app } from './agent.ts'; +import { HumanMessage } from '@langchain/core/messages'; // Reference a thread -const thread = { configurable: { thread_id: "42" }}; +const thread = { configurable: { thread_id: '42' } }; // Use the Runnable const finalState = await app.invoke( - { messages: [new HumanMessage("what is the weather in sf")] }, - thread + { messages: [new HumanMessage('what is the weather in sf')] }, + thread, ); -console.log(finalState.messages[finalState.messages.length - 1].content) +console.log(finalState.messages[finalState.messages.length - 1].content); ``` Now when we pass the same `thread_id`, in this case `"42"`, the conversation context is retained via the saved state that we've set in a local sqliteDB (i.e. stored list of messages). @@ -142,12 +141,12 @@ Also, in this next example, we demonstrate streaming output. ###### ny-weather.ts ```typescript -import {app} from './agent.ts'; +import { app } from './agent.ts'; import { HumanMessage } from '@langchain/core/messages'; const nextState = await app.invoke( - { messages: [new HumanMessage("what about ny")] }, - { configurable: { thread_id: "42"} } + { messages: [new HumanMessage('what about ny')] }, + { configurable: { thread_id: '42' } }, ); console.log(nextState.messages[nextState.messages.length - 1].content); @@ -160,7 +159,7 @@ The memory was saved in the sqlite db `./langGraph.db`. If you want to clear it, ###### clear.ts ```typescript -import {DB_NAME} from './agent.ts'; +import { DB_NAME } from './agent.ts'; import fs from 'node:fs'; // I can't find good documentation on the memory module, so let's apply the nuclear method diff --git a/packages/api/srcbook/examples/websockets.srcmd b/packages/api/srcbook/examples/websockets.src.md similarity index 100% rename from packages/api/srcbook/examples/websockets.srcmd rename to packages/api/srcbook/examples/websockets.src.md diff --git a/packages/api/srcbook/index.mts b/packages/api/srcbook/index.mts index 48aa6de5..4baf4cdc 100644 --- a/packages/api/srcbook/index.mts +++ b/packages/api/srcbook/index.mts @@ -74,7 +74,7 @@ export function writeReadmeToDisk( } /** - * Creates a srcbook directory from a .srcmd file. + * Creates a srcbook directory from a .src.md file. */ export async function importSrcbookFromSrcmdFile(srcmdPath: string) { // Check if the user is opening one of the example Srcbooks that comes bundled with the app. @@ -96,7 +96,7 @@ export async function importSrcbookFromSrcmdFile(srcmdPath: string) { } /** - * Creates a srcbook directory from a srcmd text. + * Creates a srcbook directory from srcmd text. */ export async function importSrcbookFromSrcmdText(text: string, directoryBasename?: string) { const result = decode(text); diff --git a/packages/api/srcmd/decoding.mts b/packages/api/srcmd/decoding.mts index 2a08de84..ac0b2f4d 100644 --- a/packages/api/srcmd/decoding.mts +++ b/packages/api/srcmd/decoding.mts @@ -11,7 +11,7 @@ import type { import type { DecodeCellsResult, DecodeResult } from './types.mjs'; /** - * This is used to decode a complete .srcmd file. + * This is used to decode a complete .src.md file. */ export function decode(contents: string): DecodeResult { // First, decode the markdown text into tokens. @@ -46,10 +46,10 @@ export function decode(contents: string): DecodeResult { } /** - * This is used to decode a subset of a .srcmd file. + * This is used to decode a subset of a .src.md file. * * For example, we generate a subset of a Srcbook (1 or more cells) using AI. - * When that happens, we do not have the entire .srcmd contents, so we need + * When that happens, we do not have the entire .src.md contents, so we need * to ignore some aspects of it, like parsing the metadata. */ export function decodeCells(contents: string): DecodeCellsResult { diff --git a/packages/api/srcmd/paths.mts b/packages/api/srcmd/paths.mts new file mode 100644 index 00000000..f9d9d0cd --- /dev/null +++ b/packages/api/srcmd/paths.mts @@ -0,0 +1,3 @@ +export function isSrcmdPath(path: string) { + return path.endsWith('.src.md'); +} diff --git a/packages/api/srcmd/types.mts b/packages/api/srcmd/types.mts index b7161a7c..d4ed1c2f 100644 --- a/packages/api/srcmd/types.mts +++ b/packages/api/srcmd/types.mts @@ -11,8 +11,8 @@ export type DecodeSuccessResult = { metadata: SrcbookMetadataType; }; -// This represents the result of decoding a complete .srcmd file. +// This represents the result of decoding a complete .src.md file. export type DecodeResult = DecodeErrorResult | DecodeSuccessResult; -// This represents the result of decoding a subset of content from a .srcmd file. +// This represents the result of decoding a subset of content from a .src.md file. export type DecodeCellsResult = DecodeErrorResult | Omit; diff --git a/packages/api/test/srcmd.test.mts b/packages/api/test/srcmd.test.mts index 7443a73d..e9968f34 100644 --- a/packages/api/test/srcmd.test.mts +++ b/packages/api/test/srcmd.test.mts @@ -8,7 +8,7 @@ describe('encoding and decoding srcmd files', () => { const languagePrefix = '\n\n'; beforeAll(async () => { - srcmd = await getRelativeFileContents('srcmd_files/srcbook.srcmd'); + srcmd = await getRelativeFileContents('srcmd_files/srcbook.src.md'); }); it('is an error when there is no title', () => { diff --git a/packages/api/test/srcmd_files/srcbook.srcmd b/packages/api/test/srcmd_files/srcbook.src.md similarity index 100% rename from packages/api/test/srcmd_files/srcbook.srcmd rename to packages/api/test/srcmd_files/srcbook.src.md diff --git a/packages/api/utils.mts b/packages/api/utils.mts index f53636d0..afa40bde 100644 --- a/packages/api/utils.mts +++ b/packages/api/utils.mts @@ -18,7 +18,7 @@ export async function disk(dirname: string, ext: string) { const entries = results .filter((entry) => { - return entry.isDirectory() || Path.extname(entry.name) === ext; + return entry.isDirectory() || entry.name.endsWith(ext); }) .map((entry) => { return { diff --git a/packages/web/src/components/drag-and-drop-srcmd-modal.tsx b/packages/web/src/components/drag-and-drop-srcmd-modal.tsx index 6c324a4d..4dbda796 100644 --- a/packages/web/src/components/drag-and-drop-srcmd-modal.tsx +++ b/packages/web/src/components/drag-and-drop-srcmd-modal.tsx @@ -33,7 +33,7 @@ function Modal(props: { open: boolean }) {
Open Srcbook

- Drop .srcmd file to open + Drop .src.md file to open

@@ -57,7 +57,7 @@ export function DragAndDropSrcmdModal(props: { children: React.ReactNode }) { const file = files[0]; // TODO: Error handling - if (!file.name.endsWith('.srcmd')) { + if (!file.name.endsWith('.src.md')) { return; } diff --git a/packages/web/src/components/file-picker.tsx b/packages/web/src/components/file-picker.tsx index f072db71..91258d49 100644 --- a/packages/web/src/components/file-picker.tsx +++ b/packages/web/src/components/file-picker.tsx @@ -161,7 +161,7 @@ function FsEntryItem({ export function ExportLocationPicker(props: { onSave: (directory: string, path: string) => void }) { const filenameRef = useRef(null); - const [filename, setFilename] = useState('untitled.srcmd'); + const [filename, setFilename] = useState('untitled.src.md'); const [fsResult, setFsResult] = useState({ dirname: '', entries: [] }); function onDiskResponse({ result }: DiskResponseType) { @@ -173,11 +173,11 @@ export function ExportLocationPicker(props: { onSave: (directory: string, path: const el = filenameRef.current; if (el) { - setTimeout(() => el.setSelectionRange(0, el.value.length - '.srcmd'.length), 5); + setTimeout(() => el.setSelectionRange(0, el.value.length - '.src.md'.length), 5); } }); - const validFilename = /.+\.srcmd$/.test(filename); + const validFilename = /.+\.src\.md$/.test(filename); return (
diff --git a/packages/web/src/components/import-export-srcbook-modal.tsx b/packages/web/src/components/import-export-srcbook-modal.tsx index e9475df3..97163602 100644 --- a/packages/web/src/components/import-export-srcbook-modal.tsx +++ b/packages/web/src/components/import-export-srcbook-modal.tsx @@ -55,7 +55,7 @@ export function ImportSrcbookModal({ Open Srcbook

- Open a Srcbook by importing one from a .srcmd file. + Open a Srcbook by importing one from a .src.md file.

@@ -100,7 +100,7 @@ export function ExportSrcbookModal({ Save to file

- Export this Srcbook to a .srcmd file which is shareable + Export this Srcbook to a .src.md file which is shareable and can be imported into any Srcbook application.

diff --git a/packages/web/src/components/srcbook-cards.tsx b/packages/web/src/components/srcbook-cards.tsx index 6596050d..296f2e9b 100644 --- a/packages/web/src/components/srcbook-cards.tsx +++ b/packages/web/src/components/srcbook-cards.tsx @@ -148,7 +148,7 @@ function BigButton(props: { onClick: () => void; className?: string; children: R