openwriter 0.12.0 → 0.13.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/dist/bin/pad.js CHANGED
@@ -49,162 +49,117 @@ process.stdin.on('close', () => {
49
49
  });
50
50
  // Only light imports here — helpers.js uses fs/path/os/crypto (all Node stdlib)
51
51
  import { createConnection } from 'net';
52
- import { existsSync, readFileSync } from 'fs';
53
- import { dirname, join, resolve } from 'path';
54
- import { fileURLToPath } from 'url';
55
52
  import { readConfig, saveConfig } from '../server/helpers.js';
56
- // ── Worktree-aware re-exec ──────────────────────────────────────────────────
57
- // The `openwriter` global command is npm-linked to a single repo, but during
58
- // development we use worktrees for branches. Walking up from cwd to find the
59
- // nearest openwriter repo lets each worktree's freshly-built dist take effect
60
- // automatically — no manual `npm link` swap between trees.
61
- //
62
- // Detection: walk up from process.cwd() looking for a packages/openwriter
63
- // package.json named "openwriter". If found AND it's not the same repo as the
64
- // one we're running from, dynamic-import its dist/bin/pad.js (which runs its
65
- // own server) and skip our own startup. Outside any openwriter repo (e.g. a
66
- // book project), we fall through and run our own dist normally.
67
- function findLocalOpenwriterBin() {
68
- let dir = process.cwd();
69
- while (true) {
70
- const pkgPath = join(dir, 'packages', 'openwriter', 'package.json');
71
- if (existsSync(pkgPath)) {
72
- try {
73
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
74
- if (pkg.name === 'openwriter') {
75
- const bin = join(dir, 'packages', 'openwriter', 'dist', 'bin', 'pad.js');
76
- if (existsSync(bin))
77
- return bin;
78
- }
79
- }
80
- catch { /* keep walking */ }
81
- }
82
- const parent = dirname(dir);
83
- if (parent === dir)
84
- return null;
85
- dir = parent;
86
- }
53
+ const args = process.argv.slice(2);
54
+ // Subcommands (run and exit, don't start server)
55
+ if (args[0] === 'install-skill') {
56
+ import('../server/install-skill.js').then(m => m.installSkill());
87
57
  }
88
- const myBin = resolve(fileURLToPath(import.meta.url));
89
- const localBin = findLocalOpenwriterBin();
90
- let handedOff = false;
91
- if (localBin && resolve(localBin) !== myBin) {
92
- console.error(`[OpenWriter] cwd is inside an openwriter repo — running ${localBin}`);
93
- await import(localBin);
94
- handedOff = true;
58
+ else if (args[0] === 'plugin') {
59
+ import('../server/plugin-install.js').then(m => m.handlePluginCommand(args.slice(1)));
95
60
  }
96
- if (!handedOff) {
97
- const args = process.argv.slice(2);
98
- // Subcommands (run and exit, don't start server)
99
- if (args[0] === 'install-skill') {
100
- import('../server/install-skill.js').then(m => m.installSkill());
61
+ else {
62
+ let port = 5050;
63
+ let noOpen = false;
64
+ let cliApiKey;
65
+ let cliAvUrl;
66
+ let plugins = [];
67
+ for (let i = 0; i < args.length; i++) {
68
+ if (args[i] === '--port' && args[i + 1]) {
69
+ port = parseInt(args[i + 1], 10);
70
+ i++;
71
+ }
72
+ if (args[i] === '--no-open') {
73
+ noOpen = true;
74
+ }
75
+ if (args[i] === '--api-key' && args[i + 1]) {
76
+ cliApiKey = args[i + 1];
77
+ i++;
78
+ }
79
+ if (args[i] === '--av-url' && args[i + 1]) {
80
+ cliAvUrl = args[i + 1];
81
+ i++;
82
+ }
83
+ if (args[i] === '--plugins' && args[i + 1]) {
84
+ plugins = args[i + 1].split(',').map((s) => s.trim()).filter(Boolean);
85
+ i++;
86
+ }
101
87
  }
102
- else if (args[0] === 'plugin') {
103
- import('../server/plugin-install.js').then(m => m.handlePluginCommand(args.slice(1)));
88
+ // Resolve API key: CLI flag → env var → saved config
89
+ const config = readConfig();
90
+ // Restore active profile from config
91
+ const { setActiveProfile } = await import('../server/helpers.js');
92
+ setActiveProfile(config.activeProfile || 'Default');
93
+ const avApiKey = cliApiKey || process.env.AV_API_KEY || config.avApiKey || '';
94
+ const avBackendUrl = cliAvUrl || process.env.AV_BACKEND_URL || config.avBackendUrl;
95
+ // Persist new values to config so future starts don't need them
96
+ const updates = {};
97
+ if (cliApiKey && cliApiKey !== config.avApiKey)
98
+ updates.avApiKey = cliApiKey;
99
+ if (cliAvUrl && cliAvUrl !== config.avBackendUrl)
100
+ updates.avBackendUrl = cliAvUrl;
101
+ if (Object.keys(updates).length > 0) {
102
+ saveConfig(updates);
103
+ console.log('Config saved to ~/.openwriter/config.json');
104
104
  }
105
- else {
106
- let port = 5050;
107
- let noOpen = false;
108
- let cliApiKey;
109
- let cliAvUrl;
110
- let plugins = [];
111
- for (let i = 0; i < args.length; i++) {
112
- if (args[i] === '--port' && args[i + 1]) {
113
- port = parseInt(args[i + 1], 10);
114
- i++;
115
- }
116
- if (args[i] === '--no-open') {
117
- noOpen = true;
118
- }
119
- if (args[i] === '--api-key' && args[i + 1]) {
120
- cliApiKey = args[i + 1];
121
- i++;
122
- }
123
- if (args[i] === '--av-url' && args[i + 1]) {
124
- cliAvUrl = args[i + 1];
125
- i++;
126
- }
127
- if (args[i] === '--plugins' && args[i + 1]) {
128
- plugins = args[i + 1].split(',').map((s) => s.trim()).filter(Boolean);
129
- i++;
130
- }
105
+ // Set env vars for downstream code (plugins read process.env)
106
+ if (avApiKey)
107
+ process.env.AV_API_KEY = avApiKey;
108
+ if (avBackendUrl)
109
+ process.env.AV_BACKEND_URL = avBackendUrl;
110
+ // Port check with health verification — detects orphaned servers
111
+ async function checkPort() {
112
+ const taken = await new Promise((resolve) => {
113
+ const socket = createConnection({ port, host: '127.0.0.1' });
114
+ socket.once('connect', () => { socket.destroy(); resolve(true); });
115
+ socket.once('error', () => { resolve(false); });
116
+ });
117
+ if (!taken)
118
+ return 'free';
119
+ // Port is taken — verify it's a healthy OpenWriter server
120
+ try {
121
+ const res = await fetch(`http://127.0.0.1:${port}/api/status`, { signal: AbortSignal.timeout(2000) });
122
+ return res.ok ? 'healthy' : 'orphaned';
131
123
  }
132
- // Resolve API key: CLI flag → env var → saved config
133
- const config = readConfig();
134
- // Restore active profile from config
135
- const { setActiveProfile } = await import('../server/helpers.js');
136
- setActiveProfile(config.activeProfile || 'Default');
137
- const avApiKey = cliApiKey || process.env.AV_API_KEY || config.avApiKey || '';
138
- const avBackendUrl = cliAvUrl || process.env.AV_BACKEND_URL || config.avBackendUrl;
139
- // Persist new values to config so future starts don't need them
140
- const updates = {};
141
- if (cliApiKey && cliApiKey !== config.avApiKey)
142
- updates.avApiKey = cliApiKey;
143
- if (cliAvUrl && cliAvUrl !== config.avBackendUrl)
144
- updates.avBackendUrl = cliAvUrl;
145
- if (Object.keys(updates).length > 0) {
146
- saveConfig(updates);
147
- console.log('Config saved to ~/.openwriter/config.json');
124
+ catch {
125
+ return 'orphaned';
148
126
  }
149
- // Set env vars for downstream code (plugins read process.env)
150
- if (avApiKey)
151
- process.env.AV_API_KEY = avApiKey;
152
- if (avBackendUrl)
153
- process.env.AV_BACKEND_URL = avBackendUrl;
154
- // Port check with health verification — detects orphaned servers
155
- async function checkPort() {
156
- const taken = await new Promise((resolve) => {
157
- const socket = createConnection({ port, host: '127.0.0.1' });
158
- socket.once('connect', () => { socket.destroy(); resolve(true); });
159
- socket.once('error', () => { resolve(false); });
160
- });
161
- if (!taken)
162
- return 'free';
163
- // Port is taken — verify it's a healthy OpenWriter server
164
- try {
165
- const res = await fetch(`http://127.0.0.1:${port}/api/status`, { signal: AbortSignal.timeout(2000) });
166
- return res.ok ? 'healthy' : 'orphaned';
167
- }
168
- catch {
169
- return 'orphaned';
170
- }
171
- }
172
- let portState = await checkPort();
173
- // Orphaned server: wait for it to die, then claim primary mode
127
+ }
128
+ let portState = await checkPort();
129
+ // Orphaned server: wait for it to die, then claim primary mode
130
+ if (portState === 'orphaned') {
131
+ console.error(`[OpenWriter] Port ${port} held by unresponsive process — waiting for release...`);
132
+ await new Promise(r => setTimeout(r, 3000));
133
+ portState = await checkPort();
174
134
  if (portState === 'orphaned') {
175
- console.error(`[OpenWriter] Port ${port} held by unresponsive process waiting for release...`);
135
+ // Still held — wait once more
176
136
  await new Promise(r => setTimeout(r, 3000));
177
137
  portState = await checkPort();
178
- if (portState === 'orphaned') {
179
- // Still held — wait once more
180
- await new Promise(r => setTimeout(r, 3000));
181
- portState = await checkPort();
182
- }
183
- if (portState !== 'free') {
184
- console.error(`[OpenWriter] Port ${port} still unavailable — entering client mode`);
185
- }
186
138
  }
187
- if (portState === 'healthy') {
188
- // Client mode: proxy MCP calls to existing primary server via HTTP
189
- console.error(`[OpenWriter] Port ${port} in use by healthy server — entering client mode`);
190
- const { startMcpClientServer } = await import('../server/mcp-client.js');
191
- startMcpClientServer(port).catch((err) => {
192
- console.error('[MCP-Client] Failed to start:', err);
193
- });
194
- }
195
- else {
196
- // Primary mode: start MCP stdio FIRST, then lazy-load Express
197
- const { load } = await import('../server/state.js');
198
- load();
199
- const { startMcpServer } = await import('../server/mcp.js');
200
- startMcpServer().catch((err) => {
201
- console.error('[MCP] Failed to start:', err);
202
- });
203
- // Deferred: load Express + plugins (heavy deps) after MCP is connecting
204
- const { startHttpServer } = await import('../server/index.js');
205
- startHttpServer({ port, noOpen, plugins }).catch((err) => {
206
- console.error('[HTTP] Failed to start:', err);
207
- });
139
+ if (portState !== 'free') {
140
+ console.error(`[OpenWriter] Port ${port} still unavailable entering client mode`);
208
141
  }
209
142
  }
210
- } // end if (!handedOff)
143
+ if (portState === 'healthy') {
144
+ // Client mode: proxy MCP calls to existing primary server via HTTP
145
+ console.error(`[OpenWriter] Port ${port} in use by healthy server — entering client mode`);
146
+ const { startMcpClientServer } = await import('../server/mcp-client.js');
147
+ startMcpClientServer(port).catch((err) => {
148
+ console.error('[MCP-Client] Failed to start:', err);
149
+ });
150
+ }
151
+ else {
152
+ // Primary mode: start MCP stdio FIRST, then lazy-load Express
153
+ const { load } = await import('../server/state.js');
154
+ load();
155
+ const { startMcpServer } = await import('../server/mcp.js');
156
+ startMcpServer().catch((err) => {
157
+ console.error('[MCP] Failed to start:', err);
158
+ });
159
+ // Deferred: load Express + plugins (heavy deps) after MCP is connecting
160
+ const { startHttpServer } = await import('../server/index.js');
161
+ startHttpServer({ port, noOpen, plugins }).catch((err) => {
162
+ console.error('[HTTP] Failed to start:', err);
163
+ });
164
+ }
165
+ }