diff --git a/harness-plugin-core/.gitignore b/harness-plugin-core/.gitignore new file mode 100644 index 00000000..f06235c4 --- /dev/null +++ b/harness-plugin-core/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/harness-plugin-core/CHANGELOG.md b/harness-plugin-core/CHANGELOG.md new file mode 100644 index 00000000..ca098844 --- /dev/null +++ b/harness-plugin-core/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to `@honcho-ai/harness-plugin-core` will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +This package versions independently of the Honcho API, `@honcho-ai/sdk`, and host plugins. + +## [Unreleased] diff --git a/harness-plugin-core/README.md b/harness-plugin-core/README.md new file mode 100644 index 00000000..0b4525de --- /dev/null +++ b/harness-plugin-core/README.md @@ -0,0 +1,70 @@ +# @honcho-ai/harness-plugin-core + +Shared runtime for Honcho harness plugins. + +```ts +import { loadConfig, resolveConfig } from '@honcho-ai/harness-plugin-core' + +const cfg = loadConfig({ host: 'harness' }) +// a harness can pass its plugin config as an overlay of the same six keys: +const cfg = resolveConfig(file, { host: 'harness', overlay: { workspace: 'harness', auth: { apiKey } } }) +``` + +Locally: `"@honcho-ai/harness-plugin-core": "file:../harness-plugin-core"` (bun imports the TypeScript source). + +## File shape + +```json +{ + "schemaVersion": 1, + "peerName": "user", + "workspace": "honcho", + "baseUrl": "https://api.honcho.dev", + "timeoutMs": 30000, + "auth": { "apiKey": "${HONCHO_API_KEY}" }, + "enabled": true, + "hosts": { + "test": { "workspace": "test" } + } +} +``` + +Missing `schemaVersion` is 0. On read, v0 keys (`environmentUrl`, `workspaceId`, top-level `apiKey`) are remapped in memory; the file is not rewritten. + +Resolution, highest wins: `HONCHO_*` env → overlay → `hosts.` → root → built-in. + +A host block may override the same six fields. + +Built-ins: `baseUrl = https://api.honcho.dev`, `timeoutMs = 30000`, `enabled = true`, `peerName = $USER`, `workspace` falls back to the host name. The SDK pins `/v3`; config stores the origin. + +## Telemetry headers + +Pass `telemetryHeaders()` as the SDK's `defaultHeaders`. Arbitrary headers are accepted by both the SDK and the Honcho API; missing identity fields are omitted. + +| Header | Meaning | Example | +|---|---|---| +| `X-Honcho-Host` | Agent host name, or `name/version` | `harness/1.3.13` | +| `X-Honcho-Plugin` | Honcho plugin version | `0.1.3` | +| `X-Honcho-Runtime` | This package's version (always sent) | `0.1.0` | +| `X-Honcho-Agent-Model` | The agent's completion model, not a Honcho model | `claude-sonnet-4-5` | + +```ts +import { Honcho } from '@honcho-ai/sdk' +import { loadConfig, setTelemetryHeaders, telemetryHeaders } from '@honcho-ai/harness-plugin-core' + +const cfg = loadConfig({ host: 'harness' }) +const honcho = new Honcho({ + apiKey: cfg.apiKey, + baseURL: cfg.baseUrl, + workspaceId: cfg.workspace, + timeout: cfg.timeoutMs, + defaultHeaders: telemetryHeaders({ + host: 'harness', + hostVersion: '1.3.13', + pluginVersion: '0.1.3', + model: 'claude-sonnet-4-5', + }), +}) + +setTelemetryHeaders(honcho.http.defaultHeaders, { model: 'claude-opus-4' }) +``` diff --git a/harness-plugin-core/bun.lock b/harness-plugin-core/bun.lock new file mode 100644 index 00000000..1522ce60 --- /dev/null +++ b/harness-plugin-core/bun.lock @@ -0,0 +1,25 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@honcho-ai/harness-plugin-core", + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^24.0.1", + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + } +} diff --git a/harness-plugin-core/package.json b/harness-plugin-core/package.json new file mode 100644 index 00000000..13e59083 --- /dev/null +++ b/harness-plugin-core/package.json @@ -0,0 +1,31 @@ +{ + "name": "@honcho-ai/harness-plugin-core", + "version": "0.1.0", + "description": "Shared runtime for Honcho harness plugins", + "author": "Plastic Labs ", + "license": "MIT", + "type": "module", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src", + "README.md", + "CHANGELOG.md" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/plastic-labs/honcho.git", + "directory": "harness-plugin-core" + }, + "scripts": { + "test": "bun test", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^24.0.1", + "typescript": "^5.0.0" + } +} diff --git a/harness-plugin-core/src/config.ts b/harness-plugin-core/src/config.ts new file mode 100644 index 00000000..126cdb78 --- /dev/null +++ b/harness-plugin-core/src/config.ts @@ -0,0 +1,239 @@ +import { existsSync, readFileSync } from 'node:fs' +import { homedir } from 'node:os' +import { join } from 'node:path' + +export interface AuthConfig { + apiKey?: string + oauth?: { accessToken?: string; refreshToken?: string; expiresAt?: string } +} + +/** Identity + connection + kill switch. Valid at root and as a host override. */ +export interface RootConfig { + peerName?: string + workspace?: string + baseUrl?: string + timeoutMs?: number + auth?: AuthConfig + enabled?: boolean +} + +export type HostBlock = RootConfig + +export interface FileConfig extends RootConfig { + schemaVersion?: number + hosts?: Record +} + +export interface ResolvedConfig { + host: string + peerName: string + workspace: string + baseUrl: string + timeoutMs: number + auth: AuthConfig + apiKey?: string + enabled: boolean + warnings: string[] +} + +export const DEFAULT_BASE_URL = 'https://api.honcho.dev' +export const DEFAULT_TIMEOUT_MS = 30_000 +export const CONFIG_SCHEMA_VERSION = 1 + +function isObj(v: unknown): v is Record { + return v !== null && typeof v === 'object' && !Array.isArray(v) +} + +/** Pre-schema files (no schemaVersion) → v1 keys. Host blocks included. */ +function migrate(file: unknown): Record { + if (!isObj(file)) return {} + const v = file.schemaVersion + if (typeof v === 'number' && v >= CONFIG_SCHEMA_VERSION) return { ...file } + const out: Record = { ...file } + const blocks: Record[] = [out] + if (isObj(out.hosts)) { + out.hosts = Object.fromEntries( + Object.entries(out.hosts).map(([k, block]) => { + if (!isObj(block)) return [k, block] + const next = { ...block } + blocks.push(next) + return [k, next] + }) + ) + } + for (const b of blocks) { + if (typeof b.baseUrl !== 'string') { + if (typeof b.environmentUrl === 'string') b.baseUrl = b.environmentUrl + else if (isObj(b.endpoint) && typeof b.endpoint.baseUrl === 'string') { + b.baseUrl = b.endpoint.baseUrl + } + } + if (typeof b.workspace !== 'string' && typeof b.workspaceId === 'string') { + b.workspace = b.workspaceId + } + const auth: Record = isObj(b.auth) ? { ...b.auth } : {} + if (typeof auth.apiKey !== 'string' && typeof b.apiKey === 'string') auth.apiKey = b.apiKey + if (!isObj(auth.oauth) && isObj(b.oauth)) auth.oauth = b.oauth + if (Object.keys(auth).length) b.auth = auth + delete b.environmentUrl + delete b.endpoint + delete b.workspaceId + delete b.apiKey + delete b.oauth + } + out.schemaVersion = 1 + return out +} + +function merge(base: T, over: unknown): T { + if (over === undefined || over === null) return base + if (Array.isArray(over) || !isObj(over)) return over as T + const out: Record = { ...(isObj(base) ? base : {}) } + for (const [k, v] of Object.entries(over)) { + if (v !== undefined) out[k] = k in out ? merge(out[k], v) : v + } + return out as T +} + +/** Make a value safe to pass to the SDK as `baseURL`. */ +export function normalizeBaseUrl(input: string): string { + let s = input.trim() + if (!s) return s + if (!s.startsWith('http://') && !s.startsWith('https://')) { + const host = s.split('/')[0].split(':')[0].toLowerCase() + const local = host === 'localhost' || host === '127.0.0.1' || host === '::1' + s = `${local ? 'http' : 'https'}://${s}` + } + try { + const u = new URL(s) + u.hostname = u.hostname.toLowerCase() + const path = u.pathname === '/' ? '' : u.pathname.replace(/\/+$/, '') + return `${u.protocol}//${u.host}${path}` + } catch { + return s + } +} + +function interpolate(value: string, env: NodeJS.Dict, warnings: string[]): string { + return value.replace(/\$\{([^}]+)\}/g, (m, name: string) => { + const v = env[name] + if (!v) { + warnings.push(`${m} is not set`) + return m + } + return v + }) +} + +function walkStrings(value: T, fn: (s: string) => string): T { + if (typeof value === 'string') return fn(value) as T + if (Array.isArray(value)) return value.map((x) => walkStrings(x, fn)) as T + if (isObj(value)) { + const out: Record = {} + for (const [k, v] of Object.entries(value)) out[k] = walkStrings(v, fn) + return out as T + } + return value +} + +/** Pull only the six root fields. Extra host keys (injection, observation, …) are ignored. */ +function pickRoot(block: unknown): RootConfig { + if (!isObj(block)) return {} + const auth: AuthConfig = isObj(block.auth) ? { ...(block.auth as AuthConfig) } : {} + const out: RootConfig = {} + if (typeof block.peerName === 'string') out.peerName = block.peerName + if (typeof block.workspace === 'string') out.workspace = block.workspace + if (typeof block.baseUrl === 'string') out.baseUrl = block.baseUrl + if (typeof block.timeoutMs === 'number') out.timeoutMs = block.timeoutMs + if (Object.keys(auth).length) out.auth = auth + if (typeof block.enabled === 'boolean') out.enabled = block.enabled + return out +} + +function pickHost(hosts: Record | undefined, name: string): RootConfig { + if (!hosts || !isObj(hosts[name])) return {} + return pickRoot(hosts[name]) +} + +/** + * Highest wins: HONCHO_* env → overlay → hosts. → root → built-in. + */ +export function resolveConfig( + file: unknown, + opts: { host: string; env?: NodeJS.Dict; overlay?: RootConfig } +): ResolvedConfig { + const warnings: string[] = [] + const env = opts.env ?? process.env + const host = opts.host + const raw = migrate(file) + if (typeof raw.schemaVersion === 'number' && raw.schemaVersion > CONFIG_SCHEMA_VERSION) { + warnings.push(`config schemaVersion ${raw.schemaVersion} is newer than ${CONFIG_SCHEMA_VERSION}`) + } + const hosts = isObj(raw.hosts) ? raw.hosts : undefined + + let acc: RootConfig = { + baseUrl: DEFAULT_BASE_URL, + timeoutMs: DEFAULT_TIMEOUT_MS, + enabled: true, + workspace: host, + } + acc = merge(acc, pickRoot(raw)) + acc = merge(acc, pickHost(hosts, host)) + acc = merge(acc, pickRoot(opts.overlay)) + + if (env.HONCHO_API_KEY) { + if (acc.auth?.apiKey) warnings.push('HONCHO_API_KEY shadows auth.apiKey') + acc = merge(acc, { auth: { apiKey: env.HONCHO_API_KEY } }) + } + if (env.HONCHO_BASE_URL || env.HONCHO_URL || env.HONCHO_ENDPOINT) { + const token = env.HONCHO_BASE_URL || env.HONCHO_URL || env.HONCHO_ENDPOINT || '' + acc.baseUrl = token === 'local' ? 'http://localhost:8000' : token + } + if (env.HONCHO_WORKSPACE || env.HONCHO_WORKSPACE_ID) { + acc.workspace = env.HONCHO_WORKSPACE || env.HONCHO_WORKSPACE_ID + } + if (env.HONCHO_PEER_NAME) acc.peerName = env.HONCHO_PEER_NAME + if (env.HONCHO_TIMEOUT_MS) { + const n = Number(env.HONCHO_TIMEOUT_MS) + if (Number.isFinite(n) && n > 0) acc.timeoutMs = n + } + if (env.HONCHO_ENABLED === 'false') acc.enabled = false + + acc = walkStrings(acc, (s) => interpolate(s, env, warnings)) + if (acc.baseUrl) acc.baseUrl = normalizeBaseUrl(acc.baseUrl) + + const auth = acc.auth ?? {} + return { + host, + peerName: acc.peerName || env.USER || env.USERNAME || 'user', + workspace: acc.workspace || host, + baseUrl: acc.baseUrl || DEFAULT_BASE_URL, + timeoutMs: acc.timeoutMs && acc.timeoutMs > 0 ? acc.timeoutMs : DEFAULT_TIMEOUT_MS, + auth, + apiKey: auth.apiKey, + enabled: acc.enabled !== false, + warnings, + } +} + +export function configPath(env: NodeJS.Dict = process.env): string { + return env.HONCHO_CONFIG_PATH || join(homedir(), '.honcho', 'config.json') +} + +export function loadConfig(opts: { + host: string + env?: NodeJS.Dict + overlay?: RootConfig +}): ResolvedConfig { + const env = opts.env ?? process.env + const path = configPath(env) + let file: unknown = {} + if (existsSync(path)) { + try { + file = JSON.parse(readFileSync(path, 'utf-8')) + } catch { + file = {} + } + } + return resolveConfig(file, { ...opts, env }) +} diff --git a/harness-plugin-core/src/index.ts b/harness-plugin-core/src/index.ts new file mode 100644 index 00000000..ab47a2b3 --- /dev/null +++ b/harness-plugin-core/src/index.ts @@ -0,0 +1,29 @@ +export const version = '0.1.0' + +export { + configPath, + loadConfig, + normalizeBaseUrl, + resolveConfig, + DEFAULT_BASE_URL, + DEFAULT_TIMEOUT_MS, +} from './config.ts' + +export type { + AuthConfig, + FileConfig, + HostBlock, + ResolvedConfig, + RootConfig, +} from './config.ts' + +export { + telemetryHeaders, + setTelemetryHeaders, + HEADER_AGENT_MODEL, + HEADER_HOST, + HEADER_PLUGIN, + HEADER_RUNTIME, +} from './telemetry.ts' + +export type { TelemetryIdentity } from './telemetry.ts' diff --git a/harness-plugin-core/src/telemetry.ts b/harness-plugin-core/src/telemetry.ts new file mode 100644 index 00000000..eb8b2dc3 --- /dev/null +++ b/harness-plugin-core/src/telemetry.ts @@ -0,0 +1,64 @@ +import { version } from './index.ts' + +/** Optional identity a host plugin knows at Honcho-client construction time. */ +export interface TelemetryIdentity { + /** Host app name, e.g. `cursor`, `opencode`. */ + host?: string + /** Host app version, e.g. `2026.8.1`. */ + hostVersion?: string + /** Honcho plugin version, e.g. `0.1.2`. */ + pluginVersion?: string + /** Agent completion model, e.g. `claude-sonnet-4-5`. Not a Honcho deriver/dialectic model. */ + model?: string +} + +export const HEADER_HOST = 'X-Honcho-Host' +export const HEADER_PLUGIN = 'X-Honcho-Plugin' +export const HEADER_RUNTIME = 'X-Honcho-Runtime' +export const HEADER_AGENT_MODEL = 'X-Honcho-Agent-Model' + +function sanitize(value: unknown): string | undefined { + if (typeof value !== 'string') return undefined + const s = value.replace(/[\r\n]+/g, ' ').trim() + return s || undefined +} + +function hostValue(id: TelemetryIdentity): string | undefined { + const name = sanitize(id.host) + const ver = sanitize(id.hostVersion) + if (name && ver) return `${name}/${ver}` + return name || ver +} + +/** + * Headers to pass as the SDK's `defaultHeaders`. Missing fields are omitted. + * `X-Honcho-Runtime` is always this package's version. + */ +export function telemetryHeaders( + id: TelemetryIdentity = {}, + extra?: Record +): Record { + const headers: Record = { [HEADER_RUNTIME]: version } + const host = hostValue(id) + const plugin = sanitize(id.pluginVersion) + const model = sanitize(id.model) + if (host) headers[HEADER_HOST] = host + if (plugin) headers[HEADER_PLUGIN] = plugin + if (model) headers[HEADER_AGENT_MODEL] = model + if (extra) { + for (const [k, v] of Object.entries(extra)) { + const value = sanitize(v) + if (value) headers[k] = value + } + } + return headers +} + +/** Merge identity onto a live header map (e.g. `honcho.http.defaultHeaders`). */ +export function setTelemetryHeaders( + headers: Record, + id: TelemetryIdentity = {}, + extra?: Record +): Record { + return Object.assign(headers, telemetryHeaders(id, extra)) +} diff --git a/harness-plugin-core/tests/config.test.ts b/harness-plugin-core/tests/config.test.ts new file mode 100644 index 00000000..2c1b6a3e --- /dev/null +++ b/harness-plugin-core/tests/config.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from 'bun:test' +import { normalizeBaseUrl, resolveConfig } from '../src/index.ts' + +const emptyEnv = {} + +describe('normalizeBaseUrl', () => { + test('adds https and lowercases the host', () => { + expect(normalizeBaseUrl('api.honcho.dev')).toBe('https://api.honcho.dev') + expect(normalizeBaseUrl('API.honcho.dev')).toBe('https://api.honcho.dev') + expect(normalizeBaseUrl('https://api.honcho.dev/')).toBe('https://api.honcho.dev') + }) + + test('leaves /v3 alone — the SDK owns the API version', () => { + expect(normalizeBaseUrl('https://api.honcho.dev/v3')).toBe('https://api.honcho.dev/v3') + }) + + test('localhost stays http', () => { + expect(normalizeBaseUrl('localhost:8000')).toBe('http://localhost:8000') + }) +}) + +describe('resolveConfig', () => { + test('host block beats root; env beats host', () => { + const file = { + workspace: 'root-ws', + hosts: { a: { workspace: 'host-ws' } }, + } + expect(resolveConfig(file, { host: 'a', env: emptyEnv }).workspace).toBe('host-ws') + expect( + resolveConfig(file, { host: 'a', env: { HONCHO_WORKSPACE: 'env-ws' } }).workspace + ).toBe('env-ws') + }) + + test('root apiKey / workspaceId aliases still resolve', () => { + const cfg = resolveConfig( + { apiKey: 'hch_x', workspaceId: 'from-id' }, + { host: 'a', env: emptyEnv } + ) + expect(cfg.apiKey).toBe('hch_x') + expect(cfg.workspace).toBe('from-id') + }) + + test('v1 leftover environmentUrl is ignored', () => { + const cfg = resolveConfig( + { schemaVersion: 1, baseUrl: 'https://keep.example', environmentUrl: 'https://old.example' }, + { host: 'a', env: emptyEnv } + ) + expect(cfg.baseUrl).toBe('https://keep.example') + }) + + test('overlay sits below env', () => { + expect( + resolveConfig( + {}, + { host: 'a', overlay: { workspace: 'from-overlay' }, env: { HONCHO_WORKSPACE: 'from-env' } } + ).workspace + ).toBe('from-env') + expect( + resolveConfig({}, { host: 'a', overlay: { workspace: 'from-overlay' }, env: emptyEnv }).workspace + ).toBe('from-overlay') + }) + + test('empty file uses built-ins; host name is not rewritten', () => { + const cfg = resolveConfig({}, { host: 'my-host', env: emptyEnv }) + expect(cfg.baseUrl).toBe('https://api.honcho.dev') + expect(cfg.timeoutMs).toBe(30_000) + expect(cfg.enabled).toBe(true) + expect(cfg.host).toBe('my-host') + expect(cfg.workspace).toBe('my-host') + }) +}) diff --git a/harness-plugin-core/tests/telemetry.test.ts b/harness-plugin-core/tests/telemetry.test.ts new file mode 100644 index 00000000..170c9e7a --- /dev/null +++ b/harness-plugin-core/tests/telemetry.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from 'bun:test' +import { + HEADER_AGENT_MODEL, + HEADER_HOST, + HEADER_PLUGIN, + HEADER_RUNTIME, + setTelemetryHeaders, + telemetryHeaders, + version, +} from '../src/index.ts' + +describe('telemetryHeaders', () => { + test('empty identity still sends the runtime version', () => { + expect(telemetryHeaders()).toEqual({ [HEADER_RUNTIME]: version }) + }) + + test('maps identity to headers', () => { + expect( + telemetryHeaders({ + host: 'opencode', + hostVersion: '1.3.13', + pluginVersion: '0.1.3', + model: 'claude-sonnet-4-5', + }) + ).toEqual({ + [HEADER_RUNTIME]: version, + [HEADER_HOST]: 'opencode/1.3.13', + [HEADER_PLUGIN]: '0.1.3', + [HEADER_AGENT_MODEL]: 'claude-sonnet-4-5', + }) + }) + + test('merges extra headers last, skipping blanks', () => { + const headers = telemetryHeaders({ host: 'codex', pluginVersion: '0.1.1' }, { + 'X-Custom': 'yes', + [HEADER_PLUGIN]: 'override', + 'X-Empty': ' ', + }) + expect(headers[HEADER_HOST]).toBe('codex') + expect(headers[HEADER_PLUGIN]).toBe('override') + expect(headers['X-Custom']).toBe('yes') + expect(headers).not.toHaveProperty('X-Empty') + }) +}) + +describe('setTelemetryHeaders', () => { + test('mutates an existing header map in place', () => { + const headers = telemetryHeaders({ host: 'cursor', pluginVersion: '0.1.2' }) + const returned = setTelemetryHeaders(headers, { model: 'claude-opus-4' }) + expect(returned).toBe(headers) + expect(headers[HEADER_HOST]).toBe('cursor') + expect(headers[HEADER_PLUGIN]).toBe('0.1.2') + expect(headers[HEADER_RUNTIME]).toBe(version) + expect(headers[HEADER_AGENT_MODEL]).toBe('claude-opus-4') + }) +}) diff --git a/harness-plugin-core/tsconfig.json b/harness-plugin-core/tsconfig.json new file mode 100644 index 00000000..96d10fea --- /dev/null +++ b/harness-plugin-core/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/schemas/config/v1.json b/schemas/config/v1.json new file mode 100644 index 00000000..be1384db --- /dev/null +++ b/schemas/config/v1.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://spec.honcho.dev/config/v1.json", + "type": "object", + "additionalProperties": true, + "$defs": { + "oauth": { + "type": "object", + "properties": { + "accessToken": { "type": "string" }, + "refreshToken": { "type": "string" }, + "expiresAt": { "type": "string" } + } + }, + "auth": { + "type": "object", + "properties": { + "apiKey": { "type": "string" }, + "oauth": { "$ref": "#/$defs/oauth" } + } + }, + "hostBlock": { + "type": "object", + "additionalProperties": true, + "properties": { + "peerName": { "type": "string" }, + "workspace": { "type": "string" }, + "baseUrl": { "type": "string" }, + "timeoutMs": { "type": "number" }, + "enabled": { "type": "boolean" }, + "auth": { "$ref": "#/$defs/auth" } + } + } + }, + "allOf": [{ "$ref": "#/$defs/hostBlock" }], + "properties": { + "schemaVersion": { "type": "integer", "const": 1 }, + "hosts": { + "type": "object", + "additionalProperties": { "$ref": "#/$defs/hostBlock" } + } + } +}