diff --git a/clients/intellij/package.json b/clients/intellij/package.json index b98479b..aed29ef 100644 --- a/clients/intellij/package.json +++ b/clients/intellij/package.json @@ -10,6 +10,6 @@ "devDependencies": { "cpy-cli": "^4.2.0", "rimraf": "^5.0.1", - "tabby-agent": "1.0.0" + "tabby-agent": "1.1.0-dev" } } diff --git a/clients/tabby-agent/package.json b/clients/tabby-agent/package.json index e752b26..81202ab 100644 --- a/clients/tabby-agent/package.json +++ b/clients/tabby-agent/package.json @@ -1,6 +1,6 @@ { "name": "tabby-agent", - "version": "1.0.0", + "version": "1.1.0-dev", "description": "Generic client agent for Tabby AI coding assistant IDE extensions.", "repository": "https://github.com/TabbyML/tabby", "main": "./dist/index.js", diff --git a/clients/tabby-agent/src/AgentConfig.ts b/clients/tabby-agent/src/AgentConfig.ts index a438fe9..ffac52b 100644 --- a/clients/tabby-agent/src/AgentConfig.ts +++ b/clients/tabby-agent/src/AgentConfig.ts @@ -3,6 +3,7 @@ import { isBrowser } from "./env"; export type AgentConfig = { server: { endpoint: string; + token: string; requestHeaders: Record; requestTimeout: number; }; @@ -50,6 +51,7 @@ export type PartialAgentConfig = RecursivePartial; export const defaultAgentConfig: AgentConfig = { server: { endpoint: "http://localhost:8080", + token: "", requestHeaders: {}, requestTimeout: 30000, // 30s }, @@ -82,67 +84,27 @@ export const defaultAgentConfig: AgentConfig = { }, }; -const oldConfigTomlTemplate = `## Tabby agent configuration file - -## You can uncomment any block to enable settings. -## Configurations in this file has lower priority than in IDE settings. - -## Server -## You can set the server endpoint and request timeout here. -# [server] -# endpoint = "http://localhost:8080" # http or https URL -# requestTimeout = 30000 # ms - -## You can add custom request headers, e.g. for authentication. -# [server.requestHeaders] -# Authorization = "Bearer eyJhbGciOiJ..........." - -## Completion -## You can set the prompt context to send to the server for completion. -# [completion.prompt] -# maxPrefixLines = 20 -# maxSuffixLines = 20 - -## You can set the debounce mode for auto completion requests when typing. -# [completion.debounce] -# mode = "adaptive" # or "fixed" -# interval = 250 # ms, only used when mode is "fixed" - -## You can set the timeout for completion requests. -# [completion.timeout] -# auto = 5000 # ms, for auto completion when typing -# manually = 30000 # ms, for manually triggered completion - -## Logs -## You can set the log level here. The log file is located at ~/.tabby-client/agent/logs/. -# [logs] -# level = "silent" # or "error" or "debug" - -## Anonymous usage tracking -## You can disable anonymous usage tracking here. -# [anonymousUsageTracking] -# disable = false # set to true to disable - -`; - const configTomlTemplate = `## Tabby agent configuration file ## You can uncomment any block to enable settings. ## Configurations in this file has lower priority than in IDE settings. ## Server -## You can set the server endpoint here. +## You can set the server endpoint and authentication token here. # [server] # endpoint = "http://localhost:8080" # http or https URL +# token = "your-token-here" # if server requires authentication -## You can add custom request headers, e.g. for authentication. +## You can add custom request headers. # [server.requestHeaders] -# Authorization = "Bearer eyJhbGciOiJ..........." +# Header1 = "Value1" # list your custom headers here +# Header2 = "Value2" # value can be string, number or boolean +# Authorization = "Bearer your-token-here" # if Authorization header is set, server.token will be ignored ## Logs ## You can set the log level here. The log file is located at ~/.tabby-client/agent/logs/. # [logs] -# level = "silent" # or "error" or "debug" +# level = "silent" # "silent" or "error" or "debug" ## Anonymous usage tracking ## You can disable anonymous usage tracking here. @@ -158,6 +120,7 @@ export const userAgentConfig = isBrowser const fs = require("fs-extra"); const toml = require("toml"); const chokidar = require("chokidar"); + const deepEqual = require("deep-equal"); class ConfigFile extends EventEmitter { filepath: string; @@ -177,14 +140,13 @@ export const userAgentConfig = isBrowser async load() { try { const fileContent = await fs.readFile(this.filepath, "utf8"); - // If the config file is the old template, and user has not modified it, - // Overwrite it with the new template. - if (fileContent.trim() === oldConfigTomlTemplate.trim()) { + const data = toml.parse(fileContent); + // If the config file contains no value, overwrite it with the new template. + if (Object.keys(data).length === 0 && fileContent.trim() !== configTomlTemplate.trim()) { await this.createTemplate(); - return await this.load(); + return; } - this.data = toml.parse(fileContent); - super.emit("updated", this.data); + this.data = data; } catch (error) { if (error.code === "ENOENT") { await this.createTemplate(); @@ -206,8 +168,15 @@ export const userAgentConfig = isBrowser this.watcher = chokidar.watch(this.filepath, { interval: 1000, }); - this.watcher.on("add", this.load.bind(this)); - this.watcher.on("change", this.load.bind(this)); + const onChanged = async () => { + const oldData = this.data; + await this.load(); + if (!deepEqual(oldData, this.data)) { + super.emit("updated", this.data); + } + }; + this.watcher.on("add", onChanged); + this.watcher.on("change", onChanged); } } diff --git a/clients/tabby-agent/src/Auth.ts b/clients/tabby-agent/src/Auth.ts index 7a3aa5d..ad45eaf 100644 --- a/clients/tabby-agent/src/Auth.ts +++ b/clients/tabby-agent/src/Auth.ts @@ -54,7 +54,16 @@ export class Auth extends EventEmitter { constructor(options: { endpoint: string; dataStore?: DataStore }) { super(); this.endpoint = options.endpoint; - this.dataStore = options.dataStore || dataStore; + if (options.dataStore) { + this.dataStore = options.dataStore; + } else { + this.dataStore = dataStore; + dataStore.on("updated", async () => { + await this.load(); + super.emit("updated", this.jwt); + }); + dataStore.watch(); + } this.authApi = createClient({ baseUrl: "https://app.tabbyml.com/api" }); this.scheduleRefreshToken(); } diff --git a/clients/tabby-agent/src/TabbyAgent.ts b/clients/tabby-agent/src/TabbyAgent.ts index b7f0881..51361ec 100644 --- a/clients/tabby-agent/src/TabbyAgent.ts +++ b/clients/tabby-agent/src/TabbyAgent.ts @@ -95,13 +95,13 @@ export class TabbyAgent extends EventEmitter implements Agent { this.config = deepmerge(defaultAgentConfig, this.userConfig, this.clientConfig); allLoggers.forEach((logger) => (logger.level = this.config.logs.level)); this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable; - if (this.config.server.requestHeaders["Authorization"] === undefined) { + if (isBlank(this.config.server.token) && this.config.server.requestHeaders["Authorization"] === undefined) { if (this.config.server.endpoint !== this.auth?.endpoint) { this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore }); this.auth.on("updated", this.setupApi.bind(this)); } } else { - // If `Authorization` request header is provided, use it directly. + // If auth token is provided, use it directly. this.auth = null; } await this.setupApi(); @@ -126,10 +126,15 @@ export class TabbyAgent extends EventEmitter implements Agent { } private async setupApi() { + const auth = !isBlank(this.config.server.token) + ? `Bearer ${this.config.server.token}` + : this.auth?.token + ? `Bearer ${this.auth.token}` + : undefined; this.api = createClient({ baseUrl: this.config.server.endpoint.replace(/\/+$/, ""), // remove trailing slash headers: { - Authorization: this.auth?.token ? `Bearer ${this.auth.token}` : undefined, + Authorization: auth, ...this.config.server.requestHeaders, }, }); @@ -225,6 +230,7 @@ export class TabbyAgent extends EventEmitter implements Agent { error instanceof HttpError && [401, 403, 405].indexOf(error.status) !== -1 && new URL(this.config.server.endpoint).hostname.endsWith("app.tabbyml.com") && + isBlank(this.config.server.token) && this.config.server.requestHeaders["Authorization"] === undefined ) { this.logger.debug({ requestId, path, error }, "API unauthorized"); @@ -254,8 +260,10 @@ export class TabbyAgent extends EventEmitter implements Agent { } } } catch (_) { - this.changeStatus("disconnected"); - this.serverHealthState = null; + if (this.status === "ready" || this.status === "notInitialized") { + this.changeStatus("disconnected"); + this.serverHealthState = null; + } } } diff --git a/clients/tabby-agent/src/dataStore.ts b/clients/tabby-agent/src/dataStore.ts index bbffd7f..cd515f3 100644 --- a/clients/tabby-agent/src/dataStore.ts +++ b/clients/tabby-agent/src/dataStore.ts @@ -11,28 +11,48 @@ export interface DataStore { save(): PromiseLike; } -export const dataStore: DataStore = isBrowser +export const dataStore = isBrowser ? null : (() => { - const dataFile = require("path").join(require("os").homedir(), ".tabby-client", "agent", "data.json"); + const EventEmitter = require("events"); const fs = require("fs-extra"); - return { - data: {}, - load: async function () { - await this.migrateFrom_0_3_0(); + const deepEqual = require("deep-equal"); + const chokidar = require("chokidar"); + + class FileDataStore extends EventEmitter implements FileDataStore { + filepath: string; + data: Partial = {}; + watcher: ReturnType | null = null; + + constructor(filepath: string) { + super(); + this.filepath = filepath; + } + + async load() { this.data = (await fs.readJson(dataFile, { throws: false })) || {}; - }, - save: async function () { + } + + async save() { await fs.outputJson(dataFile, this.data); - }, - migrateFrom_0_3_0: async function () { - const dataFile_0_3_0 = require("path").join(require("os").homedir(), ".tabby", "agent", "data.json"); - const migratedFlag = require("path").join(require("os").homedir(), ".tabby", "agent", ".data_json_migrated"); - if ((await fs.pathExists(dataFile_0_3_0)) && !(await fs.pathExists(migratedFlag))) { - const data = await fs.readJson(dataFile_0_3_0); - await fs.outputJson(dataFile, data); - await fs.outputFile(migratedFlag, ""); - } - }, - }; + } + + watch() { + this.watcher = chokidar.watch(this.filepath, { + interval: 1000, + }); + const onChanged = async () => { + const oldData = this.data; + await this.load(); + if (!deepEqual(oldData, this.data)) { + super.emit("updated", this.data); + } + }; + this.watcher.on("add", onChanged); + this.watcher.on("change", onChanged); + } + } + + const dataFile = require("path").join(require("os").homedir(), ".tabby-client", "agent", "data.json"); + return new FileDataStore(dataFile); })(); diff --git a/clients/vim/package.json b/clients/vim/package.json index f1b4300..29ecffe 100644 --- a/clients/vim/package.json +++ b/clients/vim/package.json @@ -10,6 +10,6 @@ "devDependencies": { "cpy-cli": "^4.2.0", "rimraf": "^5.0.1", - "tabby-agent": "1.0.0" + "tabby-agent": "1.1.0-dev" } } diff --git a/clients/vscode/package.json b/clients/vscode/package.json index f7adefc..27ecc23 100644 --- a/clients/vscode/package.json +++ b/clients/vscode/package.json @@ -7,7 +7,7 @@ "repository": "https://github.com/TabbyML/tabby", "bugs": "https://github.com/TabbyML/tabby/issues", "license": "Apache-2.0", - "version": "1.0.0", + "version": "1.1.0-dev", "keywords": [ "ai", "autocomplete", @@ -217,6 +217,6 @@ }, "dependencies": { "@xstate/fsm": "^2.0.1", - "tabby-agent": "1.0.0" + "tabby-agent": "1.1.0-dev" } }