@agent-webui/ai-desk-daemon 1.0.49-beta1 → 1.0.49-beta2

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/bin/cli.js CHANGED
@@ -12,6 +12,67 @@ const { start, stop, restart, status } = require('../lib/daemon-manager');
12
12
  const { getLogPath } = require('../lib/platform');
13
13
  const { VERSION } = require('../lib/platform');
14
14
 
15
+ function wait(ms) {
16
+ return new Promise((resolve) => setTimeout(resolve, ms));
17
+ }
18
+
19
+ function startUnixLogFollow(logPath, lines) {
20
+ const { spawn } = require('child_process');
21
+ const tailArgs = [];
22
+
23
+ if (typeof lines === 'number' && Number.isFinite(lines) && lines > 0) {
24
+ tailArgs.push('-n', String(lines));
25
+ }
26
+
27
+ tailArgs.push('-F', logPath);
28
+
29
+ const tail = spawn('tail', tailArgs);
30
+
31
+ tail.stdout.on('data', (data) => {
32
+ process.stdout.write(data);
33
+ });
34
+
35
+ tail.stderr.on('data', (data) => {
36
+ process.stderr.write(data);
37
+ });
38
+
39
+ tail.on('error', (error) => {
40
+ console.error(chalk.red('Failed to follow logs:'), error.message);
41
+ process.exit(1);
42
+ });
43
+
44
+ return tail;
45
+ }
46
+
47
+ function startWindowsLogFollow(initialLogPath) {
48
+ let currentLogPath = initialLogPath;
49
+ let lastSize = 0;
50
+
51
+ return setInterval(() => {
52
+ const nextLogPath = getLogPath();
53
+ if (nextLogPath !== currentLogPath) {
54
+ currentLogPath = nextLogPath;
55
+ lastSize = 0;
56
+ }
57
+
58
+ try {
59
+ const stats = fs.statSync(currentLogPath);
60
+ if (stats.size > lastSize) {
61
+ const stream = fs.createReadStream(currentLogPath, {
62
+ start: lastSize,
63
+ end: stats.size
64
+ });
65
+ stream.on('data', (chunk) => {
66
+ process.stdout.write(chunk);
67
+ });
68
+ lastSize = stats.size;
69
+ }
70
+ } catch (error) {
71
+ // Ignore missing files while the daemon is still starting or rolling logs.
72
+ }
73
+ }, 500);
74
+ }
75
+
15
76
  function resolveRequestedMode(options) {
16
77
  const hasExplicitMode = typeof options.mode === 'string' && options.mode.trim() !== '';
17
78
 
@@ -96,31 +157,17 @@ program
96
157
  console.log(chalk.cyan('\nšŸ“‹ Following daemon logs (Ctrl+C to stop daemon)...\n'));
97
158
 
98
159
  // Wait a moment for daemon to start writing logs
99
- await new Promise(resolve => setTimeout(resolve, 1000));
160
+ await wait(1000);
100
161
 
101
162
  const logPath = getLogPath();
102
163
  if (!fs.existsSync(logPath)) {
103
164
  console.log(chalk.yellow('Waiting for logs...'));
104
- await new Promise(resolve => setTimeout(resolve, 2000));
165
+ await wait(2000);
105
166
  }
106
167
 
107
- // Follow logs (tail -f) - works on Unix-like systems
168
+ // Follow logs - works on Unix-like systems even before the file exists.
108
169
  if (process.platform !== 'win32') {
109
- const { spawn } = require('child_process');
110
- const tail = spawn('tail', ['-f', logPath]);
111
-
112
- tail.stdout.on('data', (data) => {
113
- process.stdout.write(data);
114
- });
115
-
116
- tail.stderr.on('data', (data) => {
117
- process.stderr.write(data);
118
- });
119
-
120
- tail.on('error', (error) => {
121
- console.error(chalk.red('Failed to follow logs:'), error.message);
122
- process.exit(1);
123
- });
170
+ const tail = startUnixLogFollow(logPath);
124
171
 
125
172
  // Handle Ctrl+C - stop the daemon
126
173
  process.on('SIGINT', () => {
@@ -142,24 +189,7 @@ program
142
189
  // Windows: use polling to read log file
143
190
  console.log(chalk.yellow('Log following on Windows - press Ctrl+C to stop daemon\n'));
144
191
 
145
- let lastSize = 0;
146
- const pollInterval = setInterval(() => {
147
- try {
148
- const stats = fs.statSync(logPath);
149
- if (stats.size > lastSize) {
150
- const stream = fs.createReadStream(logPath, {
151
- start: lastSize,
152
- end: stats.size
153
- });
154
- stream.on('data', (chunk) => {
155
- process.stdout.write(chunk);
156
- });
157
- lastSize = stats.size;
158
- }
159
- } catch (error) {
160
- // Ignore errors
161
- }
162
- }, 500);
192
+ const pollInterval = startWindowsLogFollow(logPath);
163
193
 
164
194
  process.on('SIGINT', () => {
165
195
  clearInterval(pollInterval);
@@ -266,23 +296,21 @@ program
266
296
  const logPath = getLogPath();
267
297
 
268
298
  if (!fs.existsSync(logPath)) {
269
- console.log(chalk.yellow('No logs found'));
299
+ console.log(chalk.yellow(`No logs found at ${logPath}`));
270
300
  return;
271
301
  }
272
302
 
273
303
  if (options.follow) {
274
- // Follow logs (tail -f)
275
- const { spawn } = require('child_process');
276
- const tail = spawn('tail', ['-f', logPath]);
277
-
278
- tail.stdout.on('data', (data) => {
279
- process.stdout.write(data);
280
- });
281
-
282
- tail.on('error', (error) => {
283
- console.error(chalk.red('Failed to follow logs:'), error.message);
284
- process.exit(1);
285
- });
304
+ if (process.platform !== 'win32') {
305
+ startUnixLogFollow(logPath, Number.parseInt(options.lines, 10));
306
+ } else {
307
+ console.log(chalk.yellow('Following logs on Windows - press Ctrl+C to stop\n'));
308
+ const pollInterval = startWindowsLogFollow(logPath);
309
+ process.on('SIGINT', () => {
310
+ clearInterval(pollInterval);
311
+ process.exit(0);
312
+ });
313
+ }
286
314
  } else {
287
315
  // Show last N lines
288
316
  const { execSync } = require('child_process');
@@ -5,7 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const { spawn, execSync } = require('child_process');
7
7
  const http = require('http');
8
- const { getDaemonBinaryPath, getPidPath, getLogPath } = require('./platform');
8
+ const { getDaemonBinaryPath, getPidPath, getConfiguredLogPath, getLogPath } = require('./platform');
9
9
  const { getPort, getConfigPath } = require('./config');
10
10
 
11
11
  /**
@@ -95,7 +95,7 @@ function start() {
95
95
 
96
96
  const port = getPort();
97
97
  const configPath = getConfigPath();
98
- const logPath = getLogPath();
98
+ const configuredLogPath = getConfiguredLogPath();
99
99
 
100
100
  // Ensure port is a number
101
101
  const portNumber = typeof port === 'number' ? port : parseInt(port, 10);
@@ -104,7 +104,7 @@ function start() {
104
104
  }
105
105
 
106
106
  // Ensure log directory exists
107
- const logDir = require('path').dirname(logPath);
107
+ const logDir = require('path').dirname(configuredLogPath);
108
108
  if (!fs.existsSync(logDir)) {
109
109
  fs.mkdirSync(logDir, { recursive: true });
110
110
  }
@@ -124,7 +124,7 @@ function start() {
124
124
 
125
125
  console.log(`Daemon started (PID: ${child.pid})`);
126
126
  console.log(`Port: ${portNumber}`);
127
- console.log(`Logs: ${logPath}`);
127
+ console.log(`Logs: ${getLogPath()}`);
128
128
 
129
129
  return child.pid;
130
130
  }
package/lib/platform.js CHANGED
@@ -122,12 +122,140 @@ function getConfigPath() {
122
122
  }
123
123
 
124
124
  /**
125
- * Get log file path
125
+ * Expand leading "~/" against the current user's home directory.
126
126
  */
127
- function getLogPath() {
127
+ function expandHomePath(value) {
128
+ if (typeof value !== 'string') {
129
+ return '';
130
+ }
131
+
132
+ if (value === '~') {
133
+ return os.homedir();
134
+ }
135
+
136
+ if (value.startsWith(`~${path.sep}`) || value.startsWith('~/')) {
137
+ return path.join(os.homedir(), value.slice(2));
138
+ }
139
+
140
+ return value;
141
+ }
142
+
143
+ function getDefaultLogPath() {
128
144
  return path.join(getConfigDir(), 'logs', 'daemon.log');
129
145
  }
130
146
 
147
+ function getConfiguredLogPath() {
148
+ const defaultLogPath = getDefaultLogPath();
149
+ const configPath = getConfigPath();
150
+
151
+ if (!fs.existsSync(configPath)) {
152
+ return defaultLogPath;
153
+ }
154
+
155
+ try {
156
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
157
+ const configuredLogPath = typeof config?.log_path === 'string' ? config.log_path.trim() : '';
158
+ if (!configuredLogPath) {
159
+ return defaultLogPath;
160
+ }
161
+ return path.resolve(expandHomePath(configuredLogPath));
162
+ } catch {
163
+ return defaultLogPath;
164
+ }
165
+ }
166
+
167
+ function resolveBaseLogPath(baseLogPath = getConfiguredLogPath()) {
168
+ if (typeof baseLogPath !== 'string') {
169
+ return getConfiguredLogPath();
170
+ }
171
+
172
+ const trimmedPath = baseLogPath.trim();
173
+ if (!trimmedPath) {
174
+ return getConfiguredLogPath();
175
+ }
176
+
177
+ return path.resolve(expandHomePath(trimmedPath));
178
+ }
179
+
180
+ function parseRollingLogFilename(prefix, filename) {
181
+ const pattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-([0-9]{8})(?:-([0-9]+))?\\.log$`);
182
+ const match = filename.match(pattern);
183
+ if (!match) {
184
+ return null;
185
+ }
186
+
187
+ return {
188
+ date: match[1],
189
+ seq: match[2] ? Number.parseInt(match[2], 10) : 1,
190
+ };
191
+ }
192
+
193
+ function compareRollingLogEntries(left, right) {
194
+ if (left.date !== right.date) {
195
+ return left.date.localeCompare(right.date);
196
+ }
197
+ if (left.seq !== right.seq) {
198
+ return left.seq - right.seq;
199
+ }
200
+ return left.filename.localeCompare(right.filename);
201
+ }
202
+
203
+ function findLatestRollingLogPath(baseLogPath = getConfiguredLogPath()) {
204
+ const resolvedBaseLogPath = resolveBaseLogPath(baseLogPath);
205
+ const logDir = path.dirname(resolvedBaseLogPath);
206
+ const prefix = path.basename(resolvedBaseLogPath, path.extname(resolvedBaseLogPath));
207
+
208
+ if (!fs.existsSync(logDir)) {
209
+ return '';
210
+ }
211
+
212
+ const entries = [];
213
+ for (const filename of fs.readdirSync(logDir)) {
214
+ const parsed = parseRollingLogFilename(prefix, filename);
215
+ if (!parsed) {
216
+ continue;
217
+ }
218
+ entries.push({
219
+ filename,
220
+ date: parsed.date,
221
+ seq: parsed.seq,
222
+ });
223
+ }
224
+
225
+ if (entries.length === 0) {
226
+ return '';
227
+ }
228
+
229
+ entries.sort(compareRollingLogEntries);
230
+ return path.join(logDir, entries[entries.length - 1].filename);
231
+ }
232
+
233
+ function getExpectedRollingLogPath(baseLogPath = getConfiguredLogPath(), now = new Date()) {
234
+ const resolvedBaseLogPath = resolveBaseLogPath(baseLogPath);
235
+ const logDir = path.dirname(resolvedBaseLogPath);
236
+ const prefix = path.basename(resolvedBaseLogPath, path.extname(resolvedBaseLogPath));
237
+ const year = now.getFullYear().toString().padStart(4, '0');
238
+ const month = `${now.getMonth() + 1}`.padStart(2, '0');
239
+ const day = `${now.getDate()}`.padStart(2, '0');
240
+ return path.join(logDir, `${prefix}-${year}${month}${day}.log`);
241
+ }
242
+
243
+ /**
244
+ * Get the most useful daemon log path for display and tailing.
245
+ * Prefers the latest rolling log file when present.
246
+ */
247
+ function getLogPath() {
248
+ const configuredLogPath = getConfiguredLogPath();
249
+ const latestRollingLogPath = findLatestRollingLogPath(configuredLogPath);
250
+ if (latestRollingLogPath) {
251
+ return latestRollingLogPath;
252
+ }
253
+ if (fs.existsSync(configuredLogPath)) {
254
+ return configuredLogPath;
255
+ }
256
+ return getExpectedRollingLogPath(configuredLogPath);
257
+ }
258
+
131
259
  /**
132
260
  * Get PID file path
133
261
  */
@@ -149,6 +277,11 @@ module.exports = {
149
277
  getRuntimePythonPath,
150
278
  getHarnessRegistryPath,
151
279
  getConfigPath,
280
+ getConfiguredLogPath,
281
+ getDefaultLogPath,
282
+ resolveBaseLogPath,
283
+ findLatestRollingLogPath,
284
+ getExpectedRollingLogPath,
152
285
  getLogPath,
153
286
  getPidPath,
154
287
  VERSION
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-webui/ai-desk-daemon",
3
- "version": "1.0.49-beta1",
3
+ "version": "1.0.49-beta2",
4
4
  "description": "AI Desk Daemon - CLI tool for managing the AI Desk daemon service",
5
5
  "workspaces": [
6
6
  "packages/*"
@@ -39,16 +39,16 @@
39
39
  "chalk": "^4.1.2"
40
40
  },
41
41
  "optionalDependencies": {
42
- "@agent-webui/ai-desk-daemon-darwin-arm64": "1.0.49-beta1",
43
- "@agent-webui/ai-desk-daemon-darwin-x64": "1.0.49-beta1",
44
- "@agent-webui/ai-desk-daemon-linux-arm64": "1.0.49-beta1",
45
- "@agent-webui/ai-desk-daemon-linux-x64": "1.0.49-beta1",
46
- "@agent-webui/ai-desk-daemon-win32-x64": "1.0.49-beta1",
47
- "@agent-webui/ai-desk-python-darwin-arm64": "1.0.49-beta1",
48
- "@agent-webui/ai-desk-python-darwin-x64": "1.0.49-beta1",
49
- "@agent-webui/ai-desk-python-linux-arm64": "1.0.49-beta1",
50
- "@agent-webui/ai-desk-python-linux-x64": "1.0.49-beta1",
51
- "@agent-webui/ai-desk-python-win32-x64": "1.0.49-beta1"
42
+ "@agent-webui/ai-desk-daemon-darwin-arm64": "1.0.49",
43
+ "@agent-webui/ai-desk-daemon-darwin-x64": "1.0.49",
44
+ "@agent-webui/ai-desk-daemon-linux-arm64": "1.0.49",
45
+ "@agent-webui/ai-desk-daemon-linux-x64": "1.0.49",
46
+ "@agent-webui/ai-desk-daemon-win32-x64": "1.0.49",
47
+ "@agent-webui/ai-desk-python-darwin-arm64": "1.0.49",
48
+ "@agent-webui/ai-desk-python-darwin-x64": "1.0.49",
49
+ "@agent-webui/ai-desk-python-linux-arm64": "1.0.49",
50
+ "@agent-webui/ai-desk-python-linux-x64": "1.0.49",
51
+ "@agent-webui/ai-desk-python-win32-x64": "1.0.49"
52
52
  },
53
53
  "repository": {
54
54
  "type": "git",