Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
node-version: 24
- name: Install dependencies
run: npx --yes pnpm@12.0.0 install --frozen-lockfile
- name: Lint
run: npx --yes pnpm@12.0.0 lint
- name: Type-check
run: npx --yes pnpm@12.0.0 typecheck
- name: Unit tests and coverage
Expand Down Expand Up @@ -51,6 +53,8 @@ jobs:
# electron 44 has no postinstall; it downloads its binary on first use. Fetch it up front.
- name: Download Electron binary
run: node node_modules/electron/install.js
- name: Lint
run: npx --yes pnpm@12.0.0 lint
- name: Type-check
run: npx --yes pnpm@12.0.0 typecheck
- name: Unit tests
Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ node node_modules/electron/install.js
## Verify

```sh
pnpm lint # ESLint (typescript-eslint recommended) over the whole repo
pnpm typecheck # plugin boundary checks, full build, then tsc for every package and the tests
pnpm test # build, then the Vitest unit suite
pnpm test:coverage # the same suite with v8 coverage
Expand All @@ -30,7 +31,7 @@ pnpm test:e2e # build, then Playwright against the real Electron app

`pnpm test:e2e` needs a display. On Linux without one, run it under `xvfb-run -a pnpm test:e2e`. Electron only renders tray titles on macOS, so the e2e tests assert the tray title there and check the tray tooltip and menu on every platform.

CI (`.github/workflows/ci.yml`) runs on every pull request in two jobs. The macOS job runs install, `typecheck`, `test:coverage`, and `test:e2e`. The Linux job runs install, `typecheck`, `test`, and `test:e2e` under Xvfb.
CI (`.github/workflows/ci.yml`) runs on every pull request in two jobs. The macOS job runs install, `lint`, `typecheck`, `test:coverage`, and `test:e2e`. The Linux job runs install, `lint`, `typecheck`, `test`, and `test:e2e` under Xvfb.

## Installing behind a corporate or private npm feed

Expand Down Expand Up @@ -68,4 +69,4 @@ Read `docs/architecture.md` before changing `packages/kernel` or the plugin SDK.

## Commits and pull requests

Use [Conventional Commits](https://www.conventionalcommits.org/) with a scope when one fits, as the history does: `feat(graphs): ...`, `fix(azure): ...`, `docs: ...`, `test(e2e): ...`, `build: ...`, `chore: ...`. Keep each pull request to one focused change, and make sure `pnpm typecheck` and `pnpm test` pass before you open it.
Use [Conventional Commits](https://www.conventionalcommits.org/) with a scope when one fits, as the history does: `feat(graphs): ...`, `fix(azure): ...`, `docs: ...`, `test(e2e): ...`, `build: ...`, `chore: ...`. Keep each pull request to one focused change, and make sure `pnpm lint`, `pnpm typecheck`, and `pnpm test` pass before you open it.
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,16 @@ This is `examples/headless/src/main.ts`. It defines a four-plugin distribution,
```ts
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { createKernel, defineDistribution, type PluginSource } from "@borg/kernel";
import { createKernel, defineDistribution } from "@borg/kernel";
import configSqliteManifest from "@borg/plugin-config-sqlite/borg.plugin.json";
import configSqlite from "@borg/plugin-config-sqlite/main";
import helloManifest from "@borg/plugin-hello/borg.plugin.json";
import { helloGetStatus } from "@borg/plugin-hello/contract";
import hello from "@borg/plugin-hello/main";
import mockLlmManifest from "@borg/plugin-mock-llm/borg.plugin.json";
import mockLlm from "@borg/plugin-mock-llm/main";
import secretsDevManifest from "@borg/plugin-secrets-dev/borg.plugin.json";
import secretsDev from "@borg/plugin-secrets-dev/main";

const distribution = defineDistribution({
id: "example.headless",
Expand All @@ -28,10 +36,12 @@ const distribution = defineDistribution({
});
const kernel = createKernel({
distribution,
plugins: ["config-sqlite", "secrets-dev", "mock-llm", "hello"].map((name): PluginSource => ({
manifest: require(`@borg/plugin-${name}/borg.plugin.json`),
loadMain: async () => require(`@borg/plugin-${name}/main`),
})),
plugins: [
{ manifest: configSqliteManifest, loadMain: async () => configSqlite },
{ manifest: secretsDevManifest, loadMain: async () => secretsDev },
{ manifest: mockLlmManifest, loadMain: async () => mockLlm },
{ manifest: helloManifest, loadMain: async () => hello },
],
host: { dataDirectory: mkdtempSync(`${tmpdir()}/borg-headless-`) },
resolveSecretStore: async () => "borg.secrets.dev",
});
Expand Down Expand Up @@ -68,6 +78,7 @@ pnpm checks the committed lockfile against its supply-chain policies on every in
Checks:

```sh
corepack pnpm lint
corepack pnpm typecheck
corepack pnpm test
corepack pnpm test:coverage
Expand Down
100 changes: 100 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Baseline lint config: @eslint/js recommended + typescript-eslint recommended
// (syntax-only, not type-checked). The packages come from @borg/eslint-config,
// which pins typescript 6.0.3 for typescript-eslint because the repo's
// typescript 7 has no JS compiler API. Rules turned off below are listed with
// their baseline violation counts so they can be tightened later.
import { globals, js, tseslint } from "@borg/eslint-config";

export default tseslint.config(
{
// Generated or build output; hand-written source is never ignored here.
ignores: [
"**/dist/**",
"**/.package/**",
"**/node_modules/**",
"**/bundled-plugins.ts",
"**/bundled-ui-plugins.ts",
"**/coverage/**",
"**/.vite/**",
"playwright-report/**",
"test-results/**",
".research/**",
],
},
js.configs.recommended,
tseslint.configs.recommended,
{
files: ["**/*.{js,mjs,cjs}", "**/*.config.{mts,cts}"],
languageOptions: {
globals: globals.node,
},
},
{
// page.evaluate() callbacks run in the renderer.
files: ["scripts/verify-graph-designer.mjs"],
languageOptions: {
globals: { ...globals.node, ...globals.browser },
},
},
{
rules: {
// All 20 baseline no-empty hits are deliberate empty `catch {}` blocks.
"no-empty": ["error", { allowEmptyCatch: true }],
// All 12 baseline hits are intentionally `_`-prefixed discards.
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
// Off: 10 violations at baseline. Provider runtimes rethrow without
// Error.cause; adding cause changes thrown error objects. Tighten later.
"preserve-caught-error": "off",
// Off: 7 violations at baseline. Several are `let`s declared before a
// closure that assigns them; converting needs per-site review. Tighten later.
"prefer-const": "off",
},
},
{
files: ["tests/e2e/**/*.ts"],
rules: {
// Off for e2e specs: 12 violations at baseline. Playwright specs load the
// Electron binary path with require(require.resolve("electron")).
"@typescript-eslint/no-require-imports": "off",
},
},
{
files: ["packages/contracts/src/index.ts", "packages/kernel/src/tls-service.ts"],
rules: {
// Off for these files: 2 violations at baseline. Host and MCP id
// validators reject control characters on purpose.
"no-control-regex": "off",
},
},
{
files: ["packages/kernel/src/tls-service.ts"],
rules: {
// Off for this file: 1 violation at baseline. The TLS stream adapter
// aliases `this` inside ReadableStream/WritableStream callbacks.
"@typescript-eslint/no-this-alias": "off",
},
},
{
files: ["plugins/mcp-apps/src/ui.tsx"],
rules: {
// Off for this file: 1 violation at baseline. Solid's `ref={frame}`
// assigns the variable, which the rule cannot see.
"no-unassigned-vars": "off",
},
},
{
files: ["plugins/channel-imap/src/imap-codec.ts"],
rules: {
// Off for this file: 1 violation at baseline. ImapCodec.#loop is a
// write-only once-guard for the read loop.
"no-unused-private-class-members": "off",
},
},
);
20 changes: 15 additions & 5 deletions examples/headless/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { createKernel, defineDistribution, type PluginSource } from "@borg/kernel";
import { createKernel, defineDistribution } from "@borg/kernel";
import configSqliteManifest from "@borg/plugin-config-sqlite/borg.plugin.json";
import configSqlite from "@borg/plugin-config-sqlite/main";
import helloManifest from "@borg/plugin-hello/borg.plugin.json";
import { helloGetStatus } from "@borg/plugin-hello/contract";
import hello from "@borg/plugin-hello/main";
import mockLlmManifest from "@borg/plugin-mock-llm/borg.plugin.json";
import mockLlm from "@borg/plugin-mock-llm/main";
import secretsDevManifest from "@borg/plugin-secrets-dev/borg.plugin.json";
import secretsDev from "@borg/plugin-secrets-dev/main";

const distribution = defineDistribution({
id: "example.headless",
Expand All @@ -12,10 +20,12 @@ const distribution = defineDistribution({
});
const kernel = createKernel({
distribution,
plugins: ["config-sqlite", "secrets-dev", "mock-llm", "hello"].map((name): PluginSource => ({
manifest: require(`@borg/plugin-${name}/borg.plugin.json`),
loadMain: async () => require(`@borg/plugin-${name}/main`),
})),
plugins: [
{ manifest: configSqliteManifest, loadMain: async () => configSqlite },
{ manifest: secretsDevManifest, loadMain: async () => secretsDev },
{ manifest: mockLlmManifest, loadMain: async () => mockLlm },
{ manifest: helloManifest, loadMain: async () => hello },
],
host: { dataDirectory: mkdtempSync(`${tmpdir()}/borg-headless-`) },
resolveSecretStore: async () => "borg.secrets.dev",
});
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"node": ">=22"
},
"scripts": {
"lint": "eslint .",
"check:boundaries": "node scripts/check-plugin-boundaries.mjs && node scripts/check-editable-for-lists.mjs && node scripts/check-native-form-controls.mjs",
"build": "pnpm check:boundaries && pnpm -r --if-present build",
"dev": "pnpm build && node scripts/run-electron.mjs",
Expand All @@ -21,11 +22,13 @@
"verify:graph-ui": "pnpm build && node scripts/verify-graph-designer.mjs"
},
"devDependencies": {
"@borg/eslint-config": "workspace:*",
"@electron/packager": "20.3.0",
"@playwright/test": "^1.62.1",
"@types/node": "^26.3.0",
"@vitest/coverage-v8": "^4.1.11",
"electron": "^44.0.0",
"eslint": "10.11.0",
"typescript": "^7.0.2",
"vitest": "^4.1.11"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/kernel/src/model-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ export class ModelGateway {
`LLM provider ${target.providerId} registration changed`,
);
}
existing = await this.dependencies.journal.save({
await this.dependencies.journal.save({
...existing,
providerRegistrationId: registration.registrationId,
});
Expand Down
2 changes: 1 addition & 1 deletion packages/kernel/src/prompt-assembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface PromptAssemblyContext {
| undefined;
}

export interface PromptSlot extends PromptSlotContribution {}
export type PromptSlot = PromptSlotContribution;

export interface AssembledPrompt {
readonly system: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/kernel/src/tls-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ function isPlausibleHost(value: string): boolean {
return false;
}
const unbracketed = stripIpv6Brackets(value);
if (unbracketed.length === 0 || /[\[\]]/.test(unbracketed)) {
if (unbracketed.length === 0 || /[[\]]/.test(unbracketed)) {
return false;
}
if (unbracketed.includes(":")) {
Expand Down
Loading
Loading