This commit is contained in:
Dax Raad
2025-05-17 21:31:42 -04:00
parent 96fbc37f01
commit a34d020bc6
26 changed files with 979 additions and 54 deletions

34
js/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

15
js/README.md Normal file
View File

@@ -0,0 +1,15 @@
# js
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.12. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

85
js/bun.lock Normal file
View File

@@ -0,0 +1,85 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "js",
"dependencies": {
"@flystorage/file-storage": "^1.1.0",
"@flystorage/local-fs": "^1.1.0",
"ai": "^5.0.0-alpha.2",
"ulid": "^3.0.0",
"zod": "^3.24.4",
},
"devDependencies": {
"@tsconfig/bun": "^1.0.7",
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-alpha.2", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-jgRpHhpKmXnUEp41xUZyqJ8VPF9gS6W7SP2iYRaM9jaq66edcg6gTYOJLqM+nSU2tXYfkzfoBGGRvtl9ijH/VQ=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-alpha.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.2", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-oTlF6UlVitSdVPQv0e+kAkZmbuunJAUYdVEh7ZRvoti+kY/T4vOT6p22X0xTaWgl0+MI1igAT+c83j7tCMuo2w=="],
"@flystorage/dynamic-import": ["@flystorage/dynamic-import@1.0.0", "", {}, "sha512-CIbIUrBdaPFyKnkVBaqzksvzNtsMSXITR/G/6zlil3MBnPFq2LX+X4Mv5p2XOmv/3OulFs/ff2SNb+5dc2Twtg=="],
"@flystorage/file-storage": ["@flystorage/file-storage@1.1.0", "", {}, "sha512-25Gd5EsXDmhHrK5orpRuVqebQms1Cm9m5ACMZ0sVDX+Sbl1V0G88CbcWt7mEoWRYLvQ1U072htqg6Sav76ZlVA=="],
"@flystorage/local-fs": ["@flystorage/local-fs@1.1.0", "", { "dependencies": { "@flystorage/dynamic-import": "^1.0.0", "@flystorage/file-storage": "^1.1.0", "file-type": "^20.5.0", "mime-types": "^3.0.1" } }, "sha512-dbErRhqmCv2UF0zPdeH7iVWuVeTWAJHuJD/mXDe2V370/SL7XIvdE3ditBHWC+1SzBKXJ0lkykOenwlum+oqIA=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
"@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
"@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
"ai": ["ai@5.0.0-alpha.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.2", "@ai-sdk/provider-utils": "3.0.0-alpha.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-42asUyoFcqjV5AoZezJPawODCPT5Rb1y/UipVlcXn1tpqlypCchSEukjNw/l09YPVucqCbW19IVqojLttkTTVA=="],
"bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
"strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="],
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
"ulid": ["ulid@3.0.0", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-yvZYdXInnJve6LdlPIuYmURdS2NP41ZoF4QW7SXwbUKYt53+0eDAySO+rGSvM2O/ciuB/G+8N7GQrZ1mCJpuqw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
}
}

3
js/opencode.jsonc Normal file
View File

@@ -0,0 +1,3 @@
{
"lol": "jsonc"
}

19
js/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "opencode",
"type": "module",
"private": true,
"devDependencies": {
"@tsconfig/bun": "^1.0.7",
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "5"
},
"dependencies": {
"@flystorage/file-storage": "^1.1.0",
"@flystorage/local-fs": "^1.1.0",
"ai": "5.0.0-alpha.2",
"ulid": "3.0.0",
"zod": "3.24.4"
}
}

46
js/src/app/index.ts Normal file
View File

@@ -0,0 +1,46 @@
import fs from "fs/promises";
import { AppPath } from "./path";
import { Log } from "../util/log";
import { Context } from "../util/context";
export namespace App {
const log = Log.create({ service: "app" });
export type Info = Awaited<ReturnType<typeof create>>;
const ctx = Context.create<Info>("app");
export async function create(input: { directory: string }) {
log.info("creating");
const dataDir = AppPath.data(input.directory);
await fs.mkdir(dataDir, { recursive: true });
log.info("created", { path: dataDir });
const services = new Map<any, any>();
return {
get root() {
return input.directory;
},
service<T extends () => any>(service: any, init: T) {
if (!services.has(service)) {
log.info("registering service", { name: service });
services.set(service, init());
}
return services.get(service) as ReturnType<T>;
},
};
}
export function service<T extends () => any>(key: any, init: T) {
const app = ctx.use();
return app.service(key, init);
}
export async function use() {
return ctx.use();
}
export const provide = ctx.provide;
}

11
js/src/app/path.ts Normal file
View File

@@ -0,0 +1,11 @@
import path from "path";
export namespace AppPath {
export function data(input: string) {
return path.join(input, ".opencode");
}
export function storage(input: string) {
return path.join(data(input), "storage");
}
}

42
js/src/config/config.ts Normal file
View File

@@ -0,0 +1,42 @@
import path from "node:path";
import { Log } from "../util/log";
import { App } from "../app";
export namespace Config {
const log = Log.create({ service: "config" });
// TODO: this should be zod
export interface Info {
mcp: any; // TODO
lsp: any; // TODO
}
function state() {
return App.service("config", async () => {
const app = await App.use();
let result: Info = {
mcp: {},
lsp: {},
};
for (const file of ["opencode.jsonc", "opencode.json"]) {
const resolved = path.join(app.root, file);
log.info("searching", { path: resolved });
try {
result = await import(path.join(app.root, file)).then(
(mod) => mod.default,
);
log.info("found", { path: resolved });
break;
} catch (e) {
continue;
}
}
log.info("loaded", result);
return result;
});
}
function get() {
return state();
}
}

23
js/src/id/id.ts Normal file
View File

@@ -0,0 +1,23 @@
import { ulid } from "ulid";
import { z } from "zod";
export namespace Identifier {
const prefixes = {
session: "ses",
} as const;
export function create(
prefix: keyof typeof prefixes,
given?: string,
): string {
if (given) {
if (given.startsWith(prefixes[prefix])) return given;
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`);
}
return [prefixes[prefix], ulid()].join("_");
}
export function schema(prefix: keyof typeof prefixes) {
return z.string().startsWith(prefixes[prefix]);
}
}

13
js/src/index.ts Normal file
View File

@@ -0,0 +1,13 @@
import { App } from "./app";
import process from "node:process";
import { RPC } from "./server/server";
import { Session } from "./session/session";
const app = await App.create({
directory: process.cwd(),
});
App.provide(app, async () => {
const session = await Session.create();
const rpc = RPC.listen();
});

34
js/src/server/server.ts Normal file
View File

@@ -0,0 +1,34 @@
import { Log } from "../util/log";
export namespace RPC {
const log = Log.create({ service: "rpc" });
const PORT = 16713;
export function listen(input?: { port?: number }) {
const port = input?.port ?? PORT;
log.info("trying", { port });
try {
const server = Bun.serve({
port,
websocket: {
open() {},
message() {},
},
routes: {
"/ws": (req, server) => {
if (server.upgrade(req)) return;
return new Response("Not a websocket request", { status: 400 });
},
},
});
log.info("listening", { port });
return {
server,
};
} catch (e: any) {
if (e?.code === "EADDRINUSE") {
return listen({ port: port + 1 });
}
throw e;
}
}
}

22
js/src/session/session.ts Normal file
View File

@@ -0,0 +1,22 @@
import { Identifier } from "../id/id";
import { Storage } from "../storage/storage";
import { Log } from "../util/log";
export namespace Session {
const log = Log.create({ service: "session" });
export interface Info {
id: string;
title: string;
}
export async function create() {
const result: Info = {
id: Identifier.create("session"),
title: "New Session - " + new Date().toISOString(),
};
log.info("created", result);
await Storage.write("session/info/" + result.id, JSON.stringify(result));
return result;
}
}

39
js/src/storage/storage.ts Normal file
View File

@@ -0,0 +1,39 @@
import { FileStorage } from "@flystorage/file-storage";
import { LocalStorageAdapter } from "@flystorage/local-fs";
import fs from "fs/promises";
import { Log } from "../util/log";
import { App } from "../app";
import { AppPath } from "../app/path";
export namespace Storage {
const log = Log.create({ service: "storage" });
function state() {
return App.service("storage", async () => {
const app = await App.use();
const storageDir = AppPath.storage(app.root);
await fs.mkdir(storageDir, { recursive: true });
const storage = new FileStorage(new LocalStorageAdapter(storageDir));
await storage.write("test", "test");
log.info("created", { path: storageDir });
return {
storage,
};
});
}
function expose<T extends keyof FileStorage>(key: T) {
const fn = FileStorage.prototype[key];
return async (
...args: Parameters<typeof fn>
): Promise<ReturnType<typeof fn>> => {
const { storage } = await state();
const match = storage[key];
// @ts-ignore
return match.call(storage, ...args);
};
}
export const write = expose("write");
export const read = expose("read");
}

25
js/src/util/context.ts Normal file
View File

@@ -0,0 +1,25 @@
import { AsyncLocalStorage } from "node:async_hooks";
export namespace Context {
export class NotFound extends Error {
constructor(public readonly name: string) {
super(`No context found for ${name}`);
}
}
export function create<T>(name: string) {
const storage = new AsyncLocalStorage<T>();
return {
use() {
const result = storage.getStore();
if (!result) {
throw new NotFound(name);
}
return result;
},
provide<R>(value: T, fn: () => R) {
return storage.run<R>(value, fn);
},
};
}
}

27
js/src/util/log.ts Normal file
View File

@@ -0,0 +1,27 @@
export namespace Log {
export function create(tags?: Record<string, any>) {
tags = tags || {};
const result = {
info(message?: any, extra?: Record<string, any>) {
const prefix = Object.entries({
...tags,
...extra,
})
.map(([key, value]) => `${key}=${value}`)
.join(" ");
console.log(prefix, message);
return result;
},
tag(key: string, value: string) {
if (tags) tags[key] = value;
return result;
},
clone() {
return Log.create({ ...tags });
},
};
return result;
}
}

5
js/tsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {}
}