note-connector 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/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # note-connector
2
+
3
+ ChatGPT **Connector** for [note.com](https://note.com) — MCP + Apps SDK UI.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g note-connector
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ - Node.js 20+
14
+ - [uv](https://docs.astral.sh/uv/) + Python 3.13+
15
+ - Git clone of this repository (MCP server):
16
+
17
+ ```bash
18
+ git clone https://github.com/drillan/note-mcp.git
19
+ cd note-mcp && uv sync && uv run playwright install chromium
20
+ note-connector config set repoPath "$(pwd)"
21
+ ```
22
+
23
+ ## Run
24
+
25
+ ```bash
26
+ note-connector
27
+ note-connector status
28
+ note-connector stop
29
+ ```
30
+
31
+ See [full docs](https://github.com/drillan/note-mcp/blob/main/README.md).
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { Command } from "commander";
6
+ import { loadConfig, saveConfig } from "./config.js";
7
+ import { runSetup, runStart, runStartForeground } from "./runtime.js";
8
+ import { isDaemonRunning, loadRuntime, loadLastMcpAccess, clearRuntime } from "./daemon-state.js";
9
+ import { configDir } from "./paths.js";
10
+ import { APP_VERSION, isNewerVersion } from "./version.js";
11
+ const program = new Command();
12
+ program.name("note-connector").description("ChatGPT connector for note.com").version(APP_VERSION);
13
+ program
14
+ .command("start", { isDefault: true })
15
+ .description("Start note-connector in background (MCP + tunnel); terminal can close after setup")
16
+ .option("--no-tunnel", "Do not start a public tunnel")
17
+ .option("-p, --port <number>", "Local port", (v) => parseInt(v, 10))
18
+ .option("--no-open", "Do not open ChatGPT / note.com in the browser")
19
+ .option("--no-clipboard", "Do not copy Public MCP URL to clipboard")
20
+ .option("--skip-note-login", "Do not open Playwright note login")
21
+ .option("--foreground", "Keep terminal open (debug)")
22
+ .action(async (opts) => {
23
+ const base = {
24
+ noTunnel: Boolean(opts.noTunnel),
25
+ port: opts.port,
26
+ noOpen: Boolean(opts.noOpen),
27
+ noClipboard: Boolean(opts.noClipboard),
28
+ skipNoteLogin: Boolean(opts.skipNoteLogin),
29
+ };
30
+ if (opts.foreground)
31
+ await runStartForeground(base);
32
+ else
33
+ await runStart(base);
34
+ });
35
+ program
36
+ .command("setup", { hidden: true })
37
+ .description("Deprecated: use note-connector without arguments")
38
+ .action(async () => {
39
+ await runSetup();
40
+ });
41
+ program
42
+ .command("config")
43
+ .description("Get or set configuration")
44
+ .argument("[key]", "dotted key e.g. tunnel.domain")
45
+ .argument("[value]", "value to set")
46
+ .action((key, value) => {
47
+ const config = loadConfig();
48
+ if (!key) {
49
+ console.log(JSON.stringify(config, null, 2));
50
+ return;
51
+ }
52
+ if (value === undefined) {
53
+ const parts = key.split(".");
54
+ let cur = config;
55
+ for (const p of parts) {
56
+ cur = cur[p];
57
+ }
58
+ console.log(cur);
59
+ return;
60
+ }
61
+ const updated = { ...config, tunnel: { ...config.tunnel } };
62
+ if (key === "tunnel.provider")
63
+ updated.tunnel.provider = value;
64
+ else if (key === "tunnel.domain")
65
+ updated.tunnel.domain = value;
66
+ else if (key === "tunnel.subdomain")
67
+ updated.tunnel.subdomain = value;
68
+ else if (key === "tunnel.token")
69
+ updated.tunnel.token = value;
70
+ else if (key === "tunnel.publicUrl")
71
+ updated.tunnel.publicUrl = value;
72
+ else if (key === "gatewayPort")
73
+ updated.gatewayPort = parseInt(value, 10);
74
+ else if (key === "repoPath")
75
+ updated.repoPath = value;
76
+ else {
77
+ console.error(`Unknown key: ${key}`);
78
+ process.exit(1);
79
+ }
80
+ saveConfig(updated);
81
+ console.log(`Set ${key}=${value}`);
82
+ });
83
+ program
84
+ .command("update")
85
+ .description("Update note-connector to the latest npm release")
86
+ .option("--check", "only check for a newer version")
87
+ .option("--pm <manager>", "package manager (npm|pnpm|yarn|bun)", "npm")
88
+ .action((o) => {
89
+ const current = APP_VERSION;
90
+ let latest;
91
+ try {
92
+ latest = execFileSync("npm", ["view", "note-connector", "version"], { encoding: "utf8" }).trim();
93
+ }
94
+ catch {
95
+ console.error("Could not check npm for updates (package may not be published yet).\n"
96
+ + "After first release: npm install -g note-connector@latest");
97
+ process.exit(1);
98
+ }
99
+ if (!isNewerVersion(latest, current)) {
100
+ console.log(`Already up to date (v${current}).`);
101
+ return;
102
+ }
103
+ console.log(`Update available: v${current} → v${latest}`);
104
+ if (o.check) {
105
+ console.log("Run `note-connector update` to install.");
106
+ return;
107
+ }
108
+ const pm = String(o.pm ?? "npm");
109
+ const installArgs = {
110
+ npm: ["install", "-g", "note-connector@latest"],
111
+ pnpm: ["add", "-g", "note-connector@latest"],
112
+ yarn: ["global", "add", "note-connector@latest"],
113
+ bun: ["add", "-g", "note-connector@latest"],
114
+ };
115
+ const args = installArgs[pm];
116
+ if (!args) {
117
+ console.error(`Unknown package manager: ${pm}`);
118
+ process.exit(1);
119
+ }
120
+ console.log(`Installing note-connector@latest via ${pm}…`);
121
+ execFileSync(pm, args, { stdio: "inherit" });
122
+ console.log(`Updated to v${latest}.`);
123
+ });
124
+ program
125
+ .command("status")
126
+ .description("Show background server status and last ChatGPT MCP access")
127
+ .action(() => {
128
+ if (!isDaemonRunning()) {
129
+ console.log("note-connector は停止中です。起動: note-connector");
130
+ return;
131
+ }
132
+ const rt = loadRuntime();
133
+ const access = loadLastMcpAccess();
134
+ console.log("note-connector: running (background)");
135
+ if (rt) {
136
+ console.log(`PID: ${rt.cliPid}`);
137
+ console.log(`Public MCP: ${rt.publicMcpUrl}`);
138
+ console.log(`ログ: ${rt.logFile}`);
139
+ console.log(`起動: ${rt.startedAt}`);
140
+ }
141
+ if (access) {
142
+ console.log(`最終 ChatGPT 接続: ${access.at}${access.remote ? ` (${access.remote})` : ""}`);
143
+ }
144
+ });
145
+ program
146
+ .command("stop")
147
+ .description("Stop background note-connector")
148
+ .action(() => {
149
+ const rt = loadRuntime();
150
+ const pidFile = path.join(configDir(), "note-connector.pid");
151
+ const pid = rt?.cliPid ?? (fs.existsSync(pidFile) ? parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10) : NaN);
152
+ if (!pid || Number.isNaN(pid)) {
153
+ console.log("note-connector は動いていません。");
154
+ clearRuntime();
155
+ return;
156
+ }
157
+ try {
158
+ process.kill(pid, "SIGTERM");
159
+ console.log(`停止しました (PID ${pid})`);
160
+ }
161
+ catch {
162
+ console.log(`PID ${pid} は既に終了しています。`);
163
+ }
164
+ clearRuntime();
165
+ });
166
+ program.parseAsync(process.argv);
@@ -0,0 +1,6 @@
1
+ /** ChatGPT Connector setup URLs and copy (aligned with OpenAI Developer mode docs). */
2
+ export declare const CHATGPT_APPS_CONNECTORS = "https://chatgpt.com/#settings/Connectors";
3
+ export declare const CHATGPT_ADVANCED_SETTINGS = "https://chatgpt.com/#settings/Connectors/Advanced";
4
+ export declare const CONNECTOR_NAME = "note-connector";
5
+ export declare const SETUP_STEPS_JA: readonly string[];
6
+ export declare function formatSetupGuide(publicMcpUrl: string): string;
@@ -0,0 +1,58 @@
1
+ /** ChatGPT Connector setup URLs and copy (aligned with OpenAI Developer mode docs). */
2
+ export const CHATGPT_APPS_CONNECTORS = "https://chatgpt.com/#settings/Connectors";
3
+ export const CHATGPT_ADVANCED_SETTINGS = "https://chatgpt.com/#settings/Connectors/Advanced";
4
+ export const CONNECTOR_NAME = "note-connector";
5
+ export const SETUP_STEPS_JA = [
6
+ "【準備】ChatGPT の Plus / Pro / Business 等のアカウントで Web 版を開く",
7
+ "【1】設定 → アプリとコネクタ(Apps & Connectors)を開く",
8
+ "【2】高度な設定(Advanced settings)を開く",
9
+ "【3】開発者モード(Developer mode)を ON にする",
10
+ "【4】アプリとコネクタ画面に戻る",
11
+ "【5】「アプリを作成」(Create app)をクリック ※開発者モード ON のときだけ表示",
12
+ "【6】作成フォームに入力して保存:",
13
+ " ・名前: note-connector",
14
+ " ・接続 / Connector URL: (下の Public MCP URL を貼り付け)",
15
+ " ・認証: なし / No authentication(URL に key= が含まれるため)",
16
+ "【7】新しいチャット → +メニュー → 開発者モードで note-connector を有効化",
17
+ "【8】例:「note の認証状態を確認して」「下書き一覧を見せて」",
18
+ ];
19
+ export function formatSetupGuide(publicMcpUrl) {
20
+ return `# note-connector — ChatGPT 接続手順
21
+
22
+ 参照: OpenAI [Developer mode](https://platform.openai.com/docs/guides/developer-mode) / LocalAnt 同様の MCP Connector 手順
23
+
24
+ ## Public MCP URL(コピーして貼り付け)
25
+
26
+ \`\`\`
27
+ ${publicMcpUrl}
28
+ \`\`\`
29
+
30
+ ## 手順(この順番で)
31
+
32
+ 1. **ChatGPT** → [アプリとコネクタ](${CHATGPT_APPS_CONNECTORS})
33
+ 2. **[高度な設定](${CHATGPT_ADVANCED_SETTINGS})** を開く
34
+ 3. **開発者モード(Developer mode)** を **ON**
35
+ 4. 再度 [アプリとコネクタ](${CHATGPT_APPS_CONNECTORS}) へ
36
+ 5. **「アプリを作成」**(Create app)をクリック
37
+ (開発者モードが ON のときだけ表示されます)
38
+ 6. フォーム:
39
+ | 項目 | 入力 |
40
+ |------|------|
41
+ | 名前 | \`${CONNECTOR_NAME}\` |
42
+ | 接続 URL / Connector URL | 上記 Public MCP URL 全体 |
43
+ | 認証 | **なし** / **No authentication** |
44
+ 7. 保存後、下書き(Drafts)に \`${CONNECTOR_NAME}\` が表示されます
45
+ 8. **新しいチャット** → ツール/+メニュー → **開発者モード** → \`${CONNECTOR_NAME}\` を選択
46
+ 9. 会話で note 操作(初回は \`note_login\` が必要な場合あり)
47
+
48
+ ## 注意
49
+
50
+ - プロトコル: **Streaming HTTP**(\`/mcp\`)
51
+ - 認証は URL の \`?key=\` で行うため、ChatGPT 側は **認証なし** で正しいです
52
+ - 書き込みツールは ChatGPT が確認を求めることがあります(Developer mode の仕様)
53
+
54
+ ## note.com
55
+
56
+ 未ログインの場合、別タブで note.com にログインし、Playwright のログインが完了するまで待ってください。
57
+ `;
58
+ }
@@ -0,0 +1,22 @@
1
+ export type TunnelProvider = "tailscale" | "ngrok" | "cloudflared" | "localtunnel" | "none";
2
+ export interface TunnelConfig {
3
+ provider: TunnelProvider;
4
+ domain?: string;
5
+ subdomain?: string;
6
+ token?: string;
7
+ publicUrl?: string;
8
+ }
9
+ export interface NoteConnectorConfig {
10
+ gatewayPort: number;
11
+ tunnel: TunnelConfig;
12
+ /** Path to git clone with src/note_mcp (required when CLI is npm global-only). */
13
+ repoPath?: string;
14
+ }
15
+ /** Old default collided with LocalAnt — migrate on load. */
16
+ export declare const LEGACY_GATEWAY_PORT = 8787;
17
+ export declare function configPath(): string;
18
+ export declare function tokenPath(): string;
19
+ export declare function loadConfig(): NoteConnectorConfig;
20
+ export declare function saveConfig(config: NoteConnectorConfig): void;
21
+ export declare function loadOrCreateToken(): string;
22
+ export declare function buildMcpEndpoint(publicBase: string, token: string): string;
package/dist/config.js ADDED
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { configDir, ensureConfigDir } from "./paths.js";
5
+ /** Old default collided with LocalAnt — migrate on load. */
6
+ export const LEGACY_GATEWAY_PORT = 8787;
7
+ const DEFAULT_CONFIG = {
8
+ gatewayPort: 8797,
9
+ tunnel: {
10
+ provider: "tailscale",
11
+ },
12
+ };
13
+ export function configPath() {
14
+ return path.join(configDir(), "config.json");
15
+ }
16
+ export function tokenPath() {
17
+ return path.join(configDir(), "token");
18
+ }
19
+ export function loadConfig() {
20
+ const file = configPath();
21
+ if (!fs.existsSync(file)) {
22
+ return { ...DEFAULT_CONFIG, tunnel: { ...DEFAULT_CONFIG.tunnel } };
23
+ }
24
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
25
+ let gatewayPort = raw.gatewayPort ?? DEFAULT_CONFIG.gatewayPort;
26
+ if (gatewayPort === LEGACY_GATEWAY_PORT) {
27
+ gatewayPort = DEFAULT_CONFIG.gatewayPort;
28
+ saveConfig({
29
+ gatewayPort,
30
+ tunnel: { ...DEFAULT_CONFIG.tunnel, ...raw.tunnel },
31
+ });
32
+ }
33
+ return {
34
+ gatewayPort,
35
+ tunnel: { ...DEFAULT_CONFIG.tunnel, ...raw.tunnel },
36
+ };
37
+ }
38
+ export function saveConfig(config) {
39
+ ensureConfigDir();
40
+ fs.writeFileSync(configPath(), JSON.stringify(config, null, 2));
41
+ }
42
+ export function loadOrCreateToken() {
43
+ ensureConfigDir();
44
+ const file = tokenPath();
45
+ if (fs.existsSync(file)) {
46
+ const t = fs.readFileSync(file, "utf8").trim();
47
+ if (t)
48
+ return t;
49
+ }
50
+ const token = crypto.randomBytes(32).toString("base64url");
51
+ fs.writeFileSync(file, token, "utf8");
52
+ return token;
53
+ }
54
+ export function buildMcpEndpoint(publicBase, token) {
55
+ return `${publicBase.replace(/\/$/, "")}/mcp?key=${token}`;
56
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { buildMcpEndpoint } from "./config.js";
4
+ test("buildMcpEndpoint", () => {
5
+ assert.equal(buildMcpEndpoint("https://machine.tail.ts.net/", "abc"), "https://machine.tail.ts.net/mcp?key=abc");
6
+ });
@@ -0,0 +1,22 @@
1
+ export interface DaemonRuntime {
2
+ cliPid: number;
3
+ port: number;
4
+ publicMcpUrl: string;
5
+ localMcpUrl: string;
6
+ startedAt: string;
7
+ logFile: string;
8
+ tunnelProvider?: string;
9
+ }
10
+ export declare function runtimePath(): string;
11
+ export declare function cliPidPath(): string;
12
+ export declare function accessPath(): string;
13
+ export declare function loadRuntime(): DaemonRuntime | null;
14
+ export declare function saveRuntime(runtime: DaemonRuntime): void;
15
+ export declare function clearRuntime(): void;
16
+ export declare function isProcessAlive(pid: number): boolean;
17
+ export declare function isDaemonRunning(): boolean;
18
+ export interface McpAccessRecord {
19
+ at: string;
20
+ remote?: string | null;
21
+ }
22
+ export declare function loadLastMcpAccess(): McpAccessRecord | null;
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { configDir } from "./paths.js";
4
+ export function runtimePath() {
5
+ return path.join(configDir(), "runtime.json");
6
+ }
7
+ export function cliPidPath() {
8
+ return path.join(configDir(), "note-connector.pid");
9
+ }
10
+ export function accessPath() {
11
+ return path.join(configDir(), "last-mcp-access.json");
12
+ }
13
+ export function loadRuntime() {
14
+ const file = runtimePath();
15
+ if (!fs.existsSync(file))
16
+ return null;
17
+ try {
18
+ return JSON.parse(fs.readFileSync(file, "utf8"));
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function saveRuntime(runtime) {
25
+ fs.mkdirSync(configDir(), { recursive: true });
26
+ fs.writeFileSync(runtimePath(), JSON.stringify(runtime, null, 2));
27
+ }
28
+ export function clearRuntime() {
29
+ for (const f of [runtimePath(), cliPidPath(), accessPath()]) {
30
+ try {
31
+ fs.rmSync(f, { force: true });
32
+ }
33
+ catch {
34
+ /* ignore */
35
+ }
36
+ }
37
+ }
38
+ export function isProcessAlive(pid) {
39
+ try {
40
+ process.kill(pid, 0);
41
+ return true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ export function isDaemonRunning() {
48
+ const rt = loadRuntime();
49
+ if (!rt)
50
+ return false;
51
+ return isProcessAlive(rt.cliPid);
52
+ }
53
+ export function loadLastMcpAccess() {
54
+ const file = accessPath();
55
+ if (!fs.existsSync(file))
56
+ return null;
57
+ try {
58
+ return JSON.parse(fs.readFileSync(file, "utf8"));
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
@@ -0,0 +1,2 @@
1
+ import { type StartOptions } from "./runtime.js";
2
+ export declare function runDaemonWorker(opts: StartOptions, logFile: string): Promise<void>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Background worker: MCP + tunnel. Started detached by `note-connector`.
3
+ */
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { buildMcpEndpoint, loadConfig, loadOrCreateToken } from "./config.js";
7
+ import { configDir } from "./paths.js";
8
+ import { startPythonServer } from "./spawn-python.js";
9
+ import { resolveGatewayPort } from "./net.js";
10
+ import { isNoteAuthenticated, startNoteLoginInBackground } from "./note-auth.js";
11
+ import { TunnelManager } from "./tunnel/manager.js";
12
+ import { saveRuntime, cliPidPath, loadLastMcpAccess } from "./daemon-state.js";
13
+ import { ensureInitialized } from "./runtime.js";
14
+ async function verifyHealth(port) {
15
+ try {
16
+ const res = await fetch(`http://127.0.0.1:${port}/healthz`, { signal: AbortSignal.timeout(5000) });
17
+ return res.ok;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ function watchFirstChatGptAccess(sinceIso, logFile) {
24
+ let announced = false;
25
+ const tick = () => {
26
+ if (announced)
27
+ return;
28
+ const rec = loadLastMcpAccess();
29
+ if (rec && rec.at > sinceIso) {
30
+ announced = true;
31
+ const line = `\n✓ ChatGPT から MCP 接続を確認しました (${rec.at})\n`;
32
+ fs.appendFileSync(logFile, line);
33
+ process.stdout.write(line);
34
+ }
35
+ };
36
+ setInterval(tick, 2000);
37
+ }
38
+ export async function runDaemonWorker(opts, logFile) {
39
+ await ensureInitialized();
40
+ const startedAt = new Date().toISOString();
41
+ fs.writeFileSync(cliPidPath(), String(process.pid));
42
+ const config = loadConfig();
43
+ const port = await resolveGatewayPort(opts.port ?? config.gatewayPort, "127.0.0.1");
44
+ const tokenFile = path.join(configDir(), "token");
45
+ const token = loadOrCreateToken();
46
+ const tunnel = new TunnelManager();
47
+ const tunnelHostHint = config.tunnel.domain ?? (config.tunnel.publicUrl ? new URL(config.tunnel.publicUrl).host : undefined);
48
+ const child = startPythonServer({
49
+ host: "127.0.0.1",
50
+ port,
51
+ tokenFile,
52
+ tunnelHost: tunnelHostHint,
53
+ });
54
+ for (let i = 0; i < 60; i++) {
55
+ if (await verifyHealth(port))
56
+ break;
57
+ await new Promise((r) => setTimeout(r, 500));
58
+ }
59
+ if (!opts.noTunnel) {
60
+ await tunnel.start(port, config.tunnel);
61
+ }
62
+ const t = tunnel.current();
63
+ const localUrl = buildMcpEndpoint(`http://127.0.0.1:${port}`, token);
64
+ const publicUrl = t.url ? buildMcpEndpoint(t.url, token) : localUrl;
65
+ saveRuntime({
66
+ cliPid: process.pid,
67
+ port,
68
+ publicMcpUrl: publicUrl,
69
+ localMcpUrl: localUrl,
70
+ startedAt,
71
+ logFile,
72
+ tunnelProvider: t.provider,
73
+ });
74
+ if (!isNoteAuthenticated() && !opts.skipNoteLogin) {
75
+ startNoteLoginInBackground();
76
+ }
77
+ watchFirstChatGptAccess(startedAt, logFile);
78
+ child.on("exit", (code) => {
79
+ if (code !== 0 && code !== null) {
80
+ fs.appendFileSync(logFile, `\nPython MCP exited: ${code}\n`);
81
+ process.exit(code ?? 1);
82
+ }
83
+ });
84
+ const shutdown = () => {
85
+ tunnel.stop();
86
+ child.kill("SIGTERM");
87
+ try {
88
+ fs.rmSync(cliPidPath(), { force: true });
89
+ }
90
+ catch {
91
+ /* ignore */
92
+ }
93
+ process.exit(0);
94
+ };
95
+ process.on("SIGINT", shutdown);
96
+ process.on("SIGTERM", shutdown);
97
+ await new Promise(() => { });
98
+ }
99
+ const logFile = process.env.NOTE_CONNECTOR_LOG_FILE;
100
+ if (logFile && process.env.NOTE_CONNECTOR_DAEMON_WORKER === "1") {
101
+ const opts = {
102
+ noTunnel: process.env.NOTE_CONNECTOR_NO_TUNNEL === "1",
103
+ skipNoteLogin: process.env.NOTE_CONNECTOR_SKIP_NOTE_LOGIN === "1",
104
+ port: process.env.NOTE_CONNECTOR_PORT ? parseInt(process.env.NOTE_CONNECTOR_PORT, 10) : undefined,
105
+ };
106
+ runDaemonWorker(opts, logFile).catch((e) => {
107
+ fs.appendFileSync(logFile, `\n${e.stack ?? e}\n`);
108
+ process.exit(1);
109
+ });
110
+ }
@@ -0,0 +1,2 @@
1
+ import type { StartOptions } from "./runtime.js";
2
+ export declare function spawnDaemon(opts: StartOptions): Promise<void>;
package/dist/daemon.js ADDED
@@ -0,0 +1,117 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { ensureConfigDir, configDir } from "./paths.js";
6
+ import { isDaemonRunning, loadRuntime, loadLastMcpAccess, } from "./daemon-state.js";
7
+ import { runOnboarding } from "./onboarding.js";
8
+ import { isNoteAuthenticated } from "./note-auth.js";
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ function logsDir() {
11
+ const dir = path.join(configDir(), "logs");
12
+ fs.mkdirSync(dir, { recursive: true });
13
+ return dir;
14
+ }
15
+ function waitForRuntime(timeoutMs) {
16
+ return new Promise((resolve) => {
17
+ const start = Date.now();
18
+ const tick = () => {
19
+ const rt = loadRuntime();
20
+ if (rt) {
21
+ resolve(rt);
22
+ return;
23
+ }
24
+ if (Date.now() - start > timeoutMs) {
25
+ resolve(null);
26
+ return;
27
+ }
28
+ setTimeout(tick, 400);
29
+ };
30
+ tick();
31
+ });
32
+ }
33
+ function waitForChatGptAccess(sinceIso, timeoutMs) {
34
+ return new Promise((resolve) => {
35
+ const start = Date.now();
36
+ const tick = () => {
37
+ const rec = loadLastMcpAccess();
38
+ if (rec && rec.at > sinceIso) {
39
+ resolve(true);
40
+ return;
41
+ }
42
+ if (Date.now() - start > timeoutMs) {
43
+ resolve(false);
44
+ return;
45
+ }
46
+ setTimeout(tick, 1500);
47
+ };
48
+ tick();
49
+ });
50
+ }
51
+ export async function spawnDaemon(opts) {
52
+ if (isDaemonRunning()) {
53
+ const rt = loadRuntime();
54
+ console.log("note-connector はすでにバックグラウンドで動作中です。");
55
+ if (rt) {
56
+ console.log(`Public MCP: ${rt.publicMcpUrl}`);
57
+ console.log(`ログ: ${rt.logFile}`);
58
+ console.log("停止: note-connector stop / 状態: note-connector status");
59
+ }
60
+ return;
61
+ }
62
+ ensureConfigDir();
63
+ const logFile = path.join(logsDir(), "note-connector.log");
64
+ const worker = path.join(__dirname, "daemon-worker.js");
65
+ const env = {
66
+ ...process.env,
67
+ NOTE_CONNECTOR_DAEMON_WORKER: "1",
68
+ NOTE_CONNECTOR_LOG_FILE: logFile,
69
+ NOTE_CONNECTOR_NO_TUNNEL: opts.noTunnel ? "1" : "0",
70
+ NOTE_CONNECTOR_SKIP_NOTE_LOGIN: opts.skipNoteLogin ? "1" : "0",
71
+ };
72
+ if (opts.port)
73
+ env.NOTE_CONNECTOR_PORT = String(opts.port);
74
+ const out = fs.openSync(logFile, "a");
75
+ const child = spawn(process.execPath, [worker], {
76
+ detached: true,
77
+ stdio: ["ignore", out, out],
78
+ env,
79
+ });
80
+ child.unref();
81
+ console.log("バックグラウンドで起動中…");
82
+ const rt = await waitForRuntime(600_000);
83
+ if (!rt) {
84
+ console.error(`起動タイムアウト。ログ: ${logFile}`);
85
+ process.exit(1);
86
+ }
87
+ console.log("");
88
+ console.log("note-connector をバックグラウンドで起動しました");
89
+ console.log(`PID: ${rt.cliPid}`);
90
+ console.log(`ログ: ${rt.logFile}`);
91
+ console.log(`Local MCP: ${rt.localMcpUrl}`);
92
+ console.log(`Public MCP: ${rt.publicMcpUrl}`);
93
+ const noteOk = isNoteAuthenticated();
94
+ await runOnboarding({
95
+ publicMcpUrl: rt.publicMcpUrl,
96
+ localMcpUrl: rt.localMcpUrl,
97
+ noteAuthenticated: noteOk,
98
+ tunnelOk: rt.publicMcpUrl.startsWith("https://"),
99
+ noOpen: opts.noOpen,
100
+ noClipboard: opts.noClipboard,
101
+ });
102
+ console.log("");
103
+ console.log("ChatGPT からの初回接続を待っています(最大 600 秒)…");
104
+ const connected = await waitForChatGptAccess(rt.startedAt, 600_000);
105
+ if (connected) {
106
+ const rec = loadLastMcpAccess();
107
+ console.log(`✓ ChatGPT 接続成功${rec?.remote ? ` (${rec.remote})` : ""}`);
108
+ console.log("このターミナルは閉じて大丈夫です。サーバーはバックグラウンドで動き続けます。");
109
+ }
110
+ else {
111
+ console.log("まだ ChatGPT からの接続はありません。");
112
+ console.log("Connector 登録後に ChatGPT で使うと、ログに接続が記録されます。");
113
+ console.log("確認: note-connector status");
114
+ }
115
+ console.log("");
116
+ console.log("停止: note-connector stop");
117
+ }