goto-assistant 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Lau
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # goto-assistant
2
+
3
+ Lightweight, self-hosted AI assistant with first-class MCP support. Supports both Claude (Anthropic) and OpenAI as providers, with a web-based chat interface.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx goto-assistant
9
+ ```
10
+
11
+ Open http://localhost:3000 — first run redirects to setup page for API key config.
12
+
13
+ ### Requirements
14
+ - Node.js 20.11 or later
15
+ - Anthropic or OpenAI API key
16
+
17
+ ### Data Storage
18
+ All data (config, conversations, uploads) stored in `~/.goto-assistant/`.
19
+ Custom location: `GOTO_DATA_DIR=/path/to/data npx goto-assistant`
20
+
21
+ ### Custom Port
22
+ ```bash
23
+ PORT=3001 npx goto-assistant
24
+ ```
25
+
26
+ ## Architecture
27
+
28
+ ```
29
+ ┌─────────────────────────────────────────────────────────┐
30
+ │ Browser │
31
+ │ ┌──────────────┐ ┌──────────────┐ │
32
+ │ │ index.html │ │ setup.html │ │
33
+ │ │ (Chat UI) │ │ (Config) │ │
34
+ │ └──────┬───────┘ └──────┬───────┘ │
35
+ │ │ WebSocket │ HTTP POST │
36
+ └─────────┼──────────────────┼────────────────────────────┘
37
+ │ │
38
+ ┌─────────┼──────────────────┼────────────────────────────┐
39
+ │ server.ts │ │
40
+ │ ┌──────┴───────┐ ┌──────┴───────┐ │
41
+ │ │ WebSocket │ │ REST API │ │
42
+ │ │ Handler │ │ /api/* │ │
43
+ │ └──────┬───────┘ └──────────────┘ │
44
+ │ │ │
45
+ │ ┌──────┴───────┐ │
46
+ │ │ router.ts │──── routes by provider │
47
+ │ └──┬────────┬──┘ │
48
+ │ │ │ │
49
+ │ ┌──┴──┐ ┌──┴───┐ ┌────────────┐ │
50
+ │ │Claude│ │OpenAI│────▶│ MCP Servers │ │
51
+ │ │Agent │ │Agent │ │ (memory, │ │
52
+ │ │ SDK │ │ SDK │ │ fs, cron) │ │
53
+ │ └──┬──┘ └──┬───┘ └────────────┘ │
54
+ │ │ │ │
55
+ │ ┌──┴────────┴──┐ ┌────────────┐ │
56
+ │ │ sessions.ts │────▶│ SQLite DB │ │
57
+ │ │ (persistence)│ │ data/ │ │
58
+ │ └──────────────┘ └────────────┘ │
59
+ └─────────────────────────────────────────────────────────┘
60
+ ```
61
+
62
+ ## Development Setup
63
+
64
+ 1. Install dependencies:
65
+ ```bash
66
+ pnpm install
67
+ ```
68
+
69
+ 2. Start the development server:
70
+ ```bash
71
+ pnpm dev
72
+ ```
73
+
74
+ 3. Open `http://localhost:3000` — you'll be redirected to the setup page on first run to configure your AI provider and API key.
75
+
76
+ ## Configuration
77
+
78
+ App configuration is stored in `data/config.json` (created on first setup). MCP server configuration is stored separately in `data/mcp.json`. Environment variables override file config:
79
+
80
+ - `ANTHROPIC_API_KEY` — API key for Claude
81
+ - `OPENAI_API_KEY` — API key for OpenAI
82
+
83
+ ## MCP Servers
84
+
85
+ The assistant comes pre-configured with these MCP servers:
86
+
87
+ | Server | Package | Capabilities |
88
+ |--------|---------|-------------|
89
+ | **memory** | [`@modelcontextprotocol/server-memory`](https://github.com/modelcontextprotocol/servers/tree/main/src/memory) | Persistent knowledge graph across conversations |
90
+ | **filesystem** | [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) | Read, write, and manage local files |
91
+ | **cron** | [`mcp-cron`](https://github.com/jolks/mcp-cron) | Schedule shell commands and AI prompts with access to MCP servers |
92
+
93
+ Add your own through the setup page or by editing `data/mcp.json` directly. Any MCP server that supports stdio transport will work — browse the [MCP server directory](https://github.com/modelcontextprotocol/servers) for more.
94
+
95
+ ### Claude Code MCP servers
96
+
97
+ If you use [Claude Code](https://claude.ai/code), copy the example config to get MCP servers for development:
98
+
99
+ ```bash
100
+ cp .mcp.json.example .mcp.json
101
+ ```
102
+
103
+ Edit `.mcp.json` to set your preferred AI provider/model for the cron server. This file is gitignored since it contains personal preferences.
104
+
105
+ ## Development
106
+
107
+ ```bash
108
+ pnpm dev # run with tsx (hot TypeScript execution)
109
+ pnpm build # compile TypeScript to dist/
110
+ pnpm start # run compiled build
111
+ pnpm test # run tests
112
+ pnpm test:watch # run tests in watch mode
113
+ ```
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ if (!process.env.GOTO_DATA_DIR) {
6
+ process.env.GOTO_DATA_DIR = join(homedir(), ".goto-assistant");
7
+ }
8
+
9
+ await import("../dist/index.js");
@@ -0,0 +1,7 @@
1
+ import type { Config, McpServerConfig } from "../config.js";
2
+ import type { Attachment } from "./router.js";
3
+ export interface AgentResponse {
4
+ sessionId: string | null;
5
+ conversationId: string;
6
+ }
7
+ export declare function runClaude(prompt: string, config: Config, mcpServersConfig: Record<string, McpServerConfig>, onChunk: (text: string) => void, resumeSessionId?: string, attachments?: Attachment[]): Promise<AgentResponse>;
@@ -0,0 +1,75 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ import { MEMORY_FILE_PATH, MEMORY_SERVER_NAME } from "../config.js";
3
+ export async function runClaude(prompt, config, mcpServersConfig, onChunk, resumeSessionId, attachments) {
4
+ const env = {
5
+ ...process.env,
6
+ ANTHROPIC_API_KEY: config.claude.apiKey,
7
+ };
8
+ if (config.claude.baseUrl) {
9
+ env.ANTHROPIC_BASE_URL = config.claude.baseUrl;
10
+ }
11
+ // Build MCP servers config with their env vars
12
+ const mcpServers = {};
13
+ for (const [name, server] of Object.entries(mcpServersConfig)) {
14
+ const serverEnv = { ...env, ...server.env };
15
+ if (name === MEMORY_SERVER_NAME) {
16
+ serverEnv.MEMORY_FILE_PATH = MEMORY_FILE_PATH;
17
+ }
18
+ mcpServers[name] = {
19
+ command: server.command,
20
+ args: server.args,
21
+ env: serverEnv,
22
+ };
23
+ }
24
+ const options = {
25
+ model: config.claude.model,
26
+ mcpServers,
27
+ permissionMode: "bypassPermissions",
28
+ allowDangerouslySkipPermissions: true,
29
+ allowedTools: Object.keys(mcpServersConfig).map((name) => `mcp__${name}__*`),
30
+ systemPrompt: "You are a helpful personal AI assistant. You have access to MCP tools for memory, filesystem, browser automation, and scheduled tasks. Use them when appropriate. IMPORTANT: At the start of each conversation, you MUST call the memory read_graph tool to retrieve all known context about the user before responding to their first message.",
31
+ env,
32
+ maxTurns: 30,
33
+ };
34
+ if (resumeSessionId) {
35
+ options.resume = resumeSessionId;
36
+ }
37
+ // When attachments are present, reference file paths in the prompt so Claude
38
+ // can read them via the filesystem MCP server's read_media_file tool.
39
+ // The query() function only accepts a plain string prompt.
40
+ let queryPrompt = prompt;
41
+ if (attachments && attachments.length > 0) {
42
+ const paths = attachments
43
+ .filter((att) => att.filePath)
44
+ .map((att) => att.filePath);
45
+ if (paths.length > 0) {
46
+ queryPrompt = `${prompt}\n\n[The user attached ${paths.length} image(s). Read them using the filesystem read_media_file tool to see the content:\n${paths.join("\n")}]`;
47
+ }
48
+ }
49
+ let sessionId = null;
50
+ const result = query({ prompt: queryPrompt, options });
51
+ for await (const message of result) {
52
+ if (message.type === "system" && message.subtype === "init") {
53
+ sessionId = message.session_id ?? null;
54
+ }
55
+ if (message.type === "assistant") {
56
+ const content = message.content;
57
+ if (content) {
58
+ for (const block of content) {
59
+ if (block.type === "text" && block.text) {
60
+ onChunk(block.text);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ if (message.type === "result" && message.subtype === "success") {
66
+ const resultMsg = message;
67
+ sessionId = resultMsg.session_id ?? sessionId;
68
+ if (resultMsg.result) {
69
+ onChunk(resultMsg.result);
70
+ }
71
+ }
72
+ }
73
+ return { sessionId, conversationId: "" };
74
+ }
75
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Config, McpServerConfig } from "../config.js";
2
+ import type { Attachment, HistoryMessage } from "./router.js";
3
+ export declare function runOpenAI(prompt: string, config: Config, mcpServersConfig: Record<string, McpServerConfig>, onChunk: (text: string) => void, attachments?: Attachment[], history?: HistoryMessage[]): Promise<void>;
@@ -0,0 +1,109 @@
1
+ import { Agent, run, MCPServerStdio } from "@openai/agents";
2
+ import { MEMORY_FILE_PATH, MEMORY_SERVER_NAME } from "../config.js";
3
+ import { parseMessageContent } from "../sessions.js";
4
+ import { getUpload } from "../uploads.js";
5
+ export async function runOpenAI(prompt, config, mcpServersConfig, onChunk, attachments, history) {
6
+ const env = {
7
+ ...process.env,
8
+ OPENAI_API_KEY: config.openai.apiKey,
9
+ };
10
+ if (config.openai.baseUrl) {
11
+ env.OPENAI_BASE_URL = config.openai.baseUrl;
12
+ }
13
+ // Set up MCP stdio servers
14
+ const mcpServers = [];
15
+ for (const [name, server] of Object.entries(mcpServersConfig)) {
16
+ const serverEnv = { ...env, ...server.env };
17
+ if (name === MEMORY_SERVER_NAME) {
18
+ serverEnv.MEMORY_FILE_PATH = MEMORY_FILE_PATH;
19
+ }
20
+ mcpServers.push(new MCPServerStdio({
21
+ name,
22
+ fullCommand: `${server.command} ${server.args.join(" ")}`,
23
+ env: serverEnv,
24
+ }));
25
+ }
26
+ try {
27
+ // Connect all MCP servers
28
+ for (const server of mcpServers) {
29
+ await server.connect();
30
+ }
31
+ const agent = new Agent({
32
+ name: "goto-assistant",
33
+ instructions: "You are a helpful personal AI assistant. You have access to MCP tools for memory, filesystem, browser automation, and scheduled tasks. Use them when appropriate. IMPORTANT: At the start of each conversation, you MUST call the memory read_graph tool to retrieve all known context about the user before responding to their first message.",
34
+ model: config.openai.model,
35
+ mcpServers,
36
+ });
37
+ // Build conversation input with history
38
+ const inputMessages = [];
39
+ // Add previous messages (re-include images from uploads)
40
+ if (history && history.length > 0) {
41
+ for (const msg of history) {
42
+ const parsed = parseMessageContent(msg.content);
43
+ if (msg.role === "assistant") {
44
+ // OpenAI Responses API requires assistant content as an array of output blocks
45
+ inputMessages.push({
46
+ role: "assistant",
47
+ content: [{ type: "output_text", text: parsed.text }],
48
+ });
49
+ }
50
+ else if (parsed.attachments && parsed.attachments.length > 0) {
51
+ // Re-read image data for user messages that had attachments
52
+ const content = [];
53
+ for (const att of parsed.attachments) {
54
+ const upload = getUpload(att.fileId);
55
+ if (upload) {
56
+ content.push({
57
+ type: "input_image",
58
+ image: `data:${upload.mimeType};base64,${upload.data.toString("base64")}`,
59
+ });
60
+ }
61
+ }
62
+ content.push({ type: "input_text", text: parsed.text });
63
+ inputMessages.push({ role: "user", content });
64
+ }
65
+ else {
66
+ inputMessages.push({ role: msg.role, content: parsed.text });
67
+ }
68
+ }
69
+ }
70
+ // Add current message
71
+ if (attachments && attachments.length > 0) {
72
+ const content = [];
73
+ for (const att of attachments) {
74
+ content.push({
75
+ type: "input_image",
76
+ image: `data:${att.mimeType};base64,${att.data.toString("base64")}`,
77
+ });
78
+ }
79
+ content.push({ type: "input_text", text: prompt });
80
+ inputMessages.push({ role: "user", content });
81
+ }
82
+ else {
83
+ inputMessages.push({ role: "user", content: prompt });
84
+ }
85
+ const input = inputMessages.length === 1 && !history?.length && !attachments?.length ? prompt : inputMessages;
86
+ const result = await run(agent, input, { stream: true });
87
+ for await (const event of result) {
88
+ if (event.type === "raw_model_stream_event" &&
89
+ event.data?.type === "output_text_delta") {
90
+ const delta = event.data.delta;
91
+ if (delta) {
92
+ onChunk(delta);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ finally {
98
+ // Disconnect all MCP servers
99
+ for (const server of mcpServers) {
100
+ try {
101
+ await server.close();
102
+ }
103
+ catch {
104
+ // ignore cleanup errors
105
+ }
106
+ }
107
+ }
108
+ }
109
+ //# sourceMappingURL=openai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/agents/openai.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { Config, McpServerConfig } from "../config.js";
2
+ export interface Attachment {
3
+ filename: string;
4
+ mimeType: string;
5
+ data: Buffer;
6
+ filePath?: string;
7
+ }
8
+ export interface HistoryMessage {
9
+ role: string;
10
+ content: string;
11
+ }
12
+ export interface RouteResult {
13
+ sessionId: string | null;
14
+ }
15
+ export declare function routeMessage(prompt: string, config: Config, mcpServers: Record<string, McpServerConfig>, onChunk: (text: string) => void, resumeSessionId?: string, attachments?: Attachment[], history?: HistoryMessage[]): Promise<RouteResult>;
@@ -0,0 +1,17 @@
1
+ import { runClaude } from "./claude.js";
2
+ import { runOpenAI } from "./openai.js";
3
+ export async function routeMessage(prompt, config, mcpServers, onChunk, resumeSessionId, attachments, history) {
4
+ switch (config.provider) {
5
+ case "claude": {
6
+ const result = await runClaude(prompt, config, mcpServers, onChunk, resumeSessionId, attachments);
7
+ return { sessionId: result.sessionId };
8
+ }
9
+ case "openai": {
10
+ await runOpenAI(prompt, config, mcpServers, onChunk, attachments, history);
11
+ return { sessionId: null };
12
+ }
13
+ default:
14
+ throw new Error(`Unknown provider: ${config.provider}`);
15
+ }
16
+ }
17
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/agents/router.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAkBxC,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAc,EACd,MAAc,EACd,UAA2C,EAC3C,OAA+B,EAC/B,eAAwB,EACxB,WAA0B,EAC1B,OAA0B;IAE1B,QAAQ,MAAM,CAAC,QAAQ,EAAE,CAAC;QACxB,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,CAAC,CAAC;YAClG,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC;QACzC,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;YAC3E,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;QAC7B,CAAC;QACD;YACE,MAAM,IAAI,KAAK,CAAC,qBAAqB,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC"}
@@ -0,0 +1,33 @@
1
+ export interface McpServerConfig {
2
+ command: string;
3
+ args: string[];
4
+ env?: Record<string, string>;
5
+ }
6
+ export interface Config {
7
+ provider: "claude" | "openai";
8
+ claude: {
9
+ apiKey: string;
10
+ model: string;
11
+ baseUrl: string;
12
+ };
13
+ openai: {
14
+ apiKey: string;
15
+ model: string;
16
+ baseUrl: string;
17
+ };
18
+ server: {
19
+ port: number;
20
+ };
21
+ }
22
+ export declare const DATA_DIR: string;
23
+ export declare const MCP_CONFIG_PATH: string;
24
+ export declare const MEMORY_FILE_PATH: string;
25
+ export declare const MEMORY_SERVER_NAME = "memory";
26
+ export declare function isConfigured(): boolean;
27
+ export declare function loadConfig(): Config;
28
+ export declare function saveConfig(config: Config): void;
29
+ export declare function loadMcpServers(): Record<string, McpServerConfig>;
30
+ export declare function saveMcpServers(servers: Record<string, McpServerConfig>): void;
31
+ export declare function maskApiKey(key: string): string;
32
+ export declare function getMaskedConfig(config: Config): Config;
33
+ export declare function getMaskedMcpServers(servers: Record<string, McpServerConfig>): Record<string, McpServerConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,67 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export const DATA_DIR = path.join(process.cwd(), process.env.GOTO_DATA_DIR || "data");
4
+ const CONFIG_PATH = path.join(DATA_DIR, "config.json");
5
+ export const MCP_CONFIG_PATH = path.join(DATA_DIR, "mcp.json");
6
+ export const MEMORY_FILE_PATH = path.join(DATA_DIR, "memory.json");
7
+ export const MEMORY_SERVER_NAME = "memory";
8
+ export function isConfigured() {
9
+ return fs.existsSync(CONFIG_PATH);
10
+ }
11
+ export function loadConfig() {
12
+ const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
13
+ const config = JSON.parse(raw);
14
+ // Environment variables override config file values
15
+ if (process.env.ANTHROPIC_API_KEY) {
16
+ config.claude.apiKey = process.env.ANTHROPIC_API_KEY;
17
+ }
18
+ if (process.env.OPENAI_API_KEY) {
19
+ config.openai.apiKey = process.env.OPENAI_API_KEY;
20
+ }
21
+ return config;
22
+ }
23
+ export function saveConfig(config) {
24
+ if (!fs.existsSync(DATA_DIR)) {
25
+ fs.mkdirSync(DATA_DIR, { recursive: true });
26
+ }
27
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
28
+ }
29
+ export function loadMcpServers() {
30
+ if (!fs.existsSync(MCP_CONFIG_PATH))
31
+ return {};
32
+ const raw = fs.readFileSync(MCP_CONFIG_PATH, "utf-8");
33
+ const parsed = JSON.parse(raw);
34
+ return parsed.mcpServers ?? {};
35
+ }
36
+ export function saveMcpServers(servers) {
37
+ if (!fs.existsSync(DATA_DIR)) {
38
+ fs.mkdirSync(DATA_DIR, { recursive: true });
39
+ }
40
+ fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify({ mcpServers: servers }, null, 2));
41
+ }
42
+ export function maskApiKey(key) {
43
+ if (key.length <= 8)
44
+ return "****";
45
+ return key.slice(0, 4) + "****" + key.slice(-4);
46
+ }
47
+ export function getMaskedConfig(config) {
48
+ return {
49
+ ...config,
50
+ claude: { ...config.claude, apiKey: maskApiKey(config.claude.apiKey) },
51
+ openai: { ...config.openai, apiKey: maskApiKey(config.openai.apiKey) },
52
+ };
53
+ }
54
+ export function getMaskedMcpServers(servers) {
55
+ return Object.fromEntries(Object.entries(servers).map(([name, server]) => [
56
+ name,
57
+ {
58
+ ...server,
59
+ env: server.env
60
+ ? Object.fromEntries(Object.entries(server.env).map(([k, v]) => k.toLowerCase().includes("key") || k.toLowerCase().includes("secret")
61
+ ? [k, maskApiKey(v)]
62
+ : [k, v]))
63
+ : undefined,
64
+ },
65
+ ]));
66
+ }
67
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { createApp, createServer } from "./server.js";
2
+ import { isConfigured, loadConfig } from "./config.js";
3
+ const app = createApp();
4
+ const server = createServer(app);
5
+ const port = Number(process.env.PORT) || (isConfigured() ? loadConfig().server.port : 3000);
6
+ server.listen(port, () => {
7
+ console.log(`goto-assistant running at http://localhost:${port}`);
8
+ if (!isConfigured()) {
9
+ console.log("First run detected — visit the URL above to configure.");
10
+ }
11
+ });
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEvD,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC;AACxB,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;AAEjC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAE5F,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,8CAA8C,IAAI,EAAE,CAAC,CAAC;IAClE,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;QACpB,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC,CAAC;IACxE,CAAC;AACH,CAAC,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type Express } from "express";
2
+ import http from "node:http";
3
+ export declare function createApp(): Express;
4
+ export declare function createServer(app: Express): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;