idea-manager 2.3.0 → 2.4.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.
Files changed (93) hide show
  1. package/.next/build-manifest.json +2 -2
  2. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  3. package/.next/server/app/_global-error.html +2 -2
  4. package/.next/server/app/_global-error.rsc +1 -1
  5. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/server/app/_not-found.html +2 -2
  13. package/.next/server/app/_not-found.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  20. package/.next/server/app/api/advisor-actions/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/archive/route_client-reference-manifest.js +1 -1
  22. package/.next/server/app/api/filesystem/route_client-reference-manifest.js +1 -1
  23. package/.next/server/app/api/filesystem/tree/route_client-reference-manifest.js +1 -1
  24. package/.next/server/app/api/global-advisor/route.js +1 -1
  25. package/.next/server/app/api/global-advisor/route_client-reference-manifest.js +1 -1
  26. package/.next/server/app/api/global-memo/route.js +2 -2
  27. package/.next/server/app/api/global-memo/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/maintenance/route.js +2 -2
  30. package/.next/server/app/api/maintenance/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/projects/[id]/advisor/route.js +1 -1
  32. package/.next/server/app/api/projects/[id]/advisor/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/projects/[id]/apply-distribute/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/projects/[id]/auto-distribute/route.js +2 -2
  35. package/.next/server/app/api/projects/[id]/auto-distribute/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/projects/[id]/brainstorm/route.js +2 -2
  37. package/.next/server/app/api/projects/[id]/brainstorm/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/projects/[id]/git-sync/route.js +2 -2
  39. package/.next/server/app/api/projects/[id]/git-sync/route_client-reference-manifest.js +1 -1
  40. package/.next/server/app/api/projects/[id]/route.js +2 -2
  41. package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route.js +2 -2
  43. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.js +1 -1
  45. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route_client-reference-manifest.js +1 -1
  46. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.js +2 -2
  47. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route_client-reference-manifest.js +1 -1
  48. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route.js +1 -1
  49. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  51. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route.js +2 -2
  52. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route_client-reference-manifest.js +1 -1
  54. package/.next/server/app/api/projects/[id]/sub-projects/route.js +2 -2
  55. package/.next/server/app/api/projects/[id]/sub-projects/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/projects/route.js +2 -2
  57. package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  58. package/.next/server/app/api/search/route.js +2 -2
  59. package/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  60. package/.next/server/app/api/sync/route.js +2 -2
  61. package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/tasks/[taskId]/move/route_client-reference-manifest.js +1 -1
  63. package/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  64. package/.next/server/app/api/version/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/index.html +2 -2
  66. package/.next/server/app/index.rsc +2 -2
  67. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  68. package/.next/server/app/index.segments/_full.segment.rsc +2 -2
  69. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  70. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  71. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  72. package/.next/server/app/page.js +3 -3
  73. package/.next/server/app/page_client-reference-manifest.js +1 -1
  74. package/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app-paths-manifest.json +10 -10
  76. package/.next/server/chunks/697.js +2 -2
  77. package/.next/server/pages/404.html +2 -2
  78. package/.next/server/pages/500.html +2 -2
  79. package/.next/static/chunks/app/{page-16b10bcc10c22811.js → page-835e6af14011d7a8.js} +9 -9
  80. package/bin/postinstall.js +27 -7
  81. package/package.json +1 -1
  82. package/src/cli.ts +46 -13
  83. package/src/components/dashboard/DashboardPanel.tsx +13 -0
  84. package/src/components/memo/GlobalMemoLayer.tsx +15 -0
  85. package/src/components/ui/FileTreeDrawer.tsx +1 -1
  86. package/src/lib/ai/client.ts +21 -7
  87. package/src/lib/auto-update.ts +22 -3
  88. package/src/lib/db/index.ts +29 -5
  89. package/src/lib/scheduler.ts +9 -3
  90. package/src/lib/sync/git.ts +17 -14
  91. package/src/lib/watcher.ts +4 -4
  92. /package/.next/static/{E6_zErdSfG_2YXZzHtQ34 → 8qtkIvEkJ-lmfHkJP6hlo}/_buildManifest.js +0 -0
  93. /package/.next/static/{E6_zErdSfG_2YXZzHtQ34 → 8qtkIvEkJ-lmfHkJP6hlo}/_ssgManifest.js +0 -0
@@ -1,15 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { execSync } = require('child_process');
4
+ const path = require('path');
5
+
6
+ const isWindows = process.platform === 'win32';
4
7
 
5
8
  try {
6
- execSync('im --version', { stdio: 'ignore' });
9
+ // Check if `im` is on PATH. Windows uses `where`, POSIX uses `command -v`.
10
+ execSync(isWindows ? 'where im' : 'command -v im', { stdio: 'ignore', shell: true });
7
11
  } catch {
8
- const npmBin = execSync('npm prefix -g', { encoding: 'utf-8' }).trim() + '/bin';
12
+ let npmPrefix = '';
13
+ try {
14
+ npmPrefix = execSync('npm prefix -g', { encoding: 'utf-8' }).trim();
15
+ } catch { /* fall through */ }
16
+
17
+ console.log('\n \u2713 idea-manager installed!\n');
18
+ console.log(' \u26A0 "im" command not found in PATH.\n');
9
19
 
10
- console.log('\n ✓ idea-manager installed!\n');
11
- console.log(' ⚠ "im" command not found in PATH.');
12
- console.log(` Add this to your shell profile (~/.zshrc or ~/.bashrc):\n`);
13
- console.log(` export PATH="${npmBin}:$PATH"\n`);
14
- console.log(' Then run: source ~/.zshrc\n');
20
+ if (isWindows) {
21
+ // On Windows, npm global prefix is the directory holding the .cmd wrappers (no "bin" subdir).
22
+ const dir = npmPrefix || '%APPDATA%\\npm';
23
+ console.log(` Add this directory to your PATH:\n`);
24
+ console.log(` ${dir}\n`);
25
+ console.log(' PowerShell (User PATH):');
26
+ console.log(` [Environment]::SetEnvironmentVariable('Path', "$env:Path;${dir}", 'User')\n`);
27
+ console.log(' Or via System Properties > Environment Variables.');
28
+ console.log(' Open a new terminal and run: im start\n');
29
+ } else {
30
+ const binDir = npmPrefix ? path.join(npmPrefix, 'bin') : '$(npm prefix -g)/bin';
31
+ console.log(' Add to your shell profile (~/.zshrc or ~/.bashrc):\n');
32
+ console.log(` export PATH="${binDir}:$PATH"\n`);
33
+ console.log(' Then open a new terminal and run: im start\n');
34
+ }
15
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idea-manager",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "AI-powered idea & task manager for developers. Brainstorm → structured tasks → AI agent execution. CodeMirror editor with slash commands, MCP Server, Claude/Gemini/Codex support. Local-first SQLite, cross-PC Git sync.",
5
5
  "keywords": [
6
6
  "idea-manager",
package/src/cli.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Force UTF-8 output on Windows so Korean strings don't garble in cp949 terminals.
4
+ if (process.platform === 'win32') {
5
+ try {
6
+ process.stdout.setDefaultEncoding?.('utf8');
7
+ process.stderr.setDefaultEncoding?.('utf8');
8
+ } catch { /* non-critical */ }
9
+ }
10
+
3
11
  import { Command } from 'commander';
4
12
  import { ensureDb } from './lib/db';
5
13
  import { startMcpServer } from './lib/mcp/server';
@@ -12,6 +20,7 @@ import { startWatcher } from './lib/watcher';
12
20
  import { syncInit, syncPush, syncPull, syncStatus } from './lib/sync/index';
13
21
  import { maybeAutoUpdate, respawnSelf } from './lib/auto-update';
14
22
  import { spawn } from 'child_process';
23
+ import { readFileSync } from 'fs';
15
24
  import path from 'path';
16
25
  import { fileURLToPath } from 'url';
17
26
 
@@ -41,6 +50,9 @@ async function openAsApp(url: string) {
41
50
  ]
42
51
  : platform === 'win32'
43
52
  ? [
53
+ { bin: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', args: [`--app=${url}`] },
54
+ { bin: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', args: [`--app=${url}`] },
55
+ { bin: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', args: [`--app=${url}`] },
44
56
  { bin: 'chrome', args: [`--app=${url}`] },
45
57
  { bin: 'msedge', args: [`--app=${url}`] },
46
58
  ]
@@ -51,7 +63,9 @@ async function openAsApp(url: string) {
51
63
  ];
52
64
 
53
65
  for (const browser of browsers) {
54
- if (platform === 'darwin' && !fs.existsSync(browser.bin)) continue;
66
+ // Skip absolute paths that don't exist (macOS .app, Windows Program Files)
67
+ const isAbsolute = path.isAbsolute(browser.bin);
68
+ if (isAbsolute && !fs.existsSync(browser.bin)) continue;
55
69
 
56
70
  try {
57
71
  const child = spawnChild(browser.bin, browser.args, {
@@ -73,10 +87,17 @@ async function openAsApp(url: string) {
73
87
 
74
88
  const program = new Command();
75
89
 
90
+ function readPkgVersion(): string {
91
+ try {
92
+ const pkg = JSON.parse(readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')) as { version?: string };
93
+ return pkg.version ?? '0.0.0';
94
+ } catch { return '0.0.0'; }
95
+ }
96
+
76
97
  program
77
98
  .name('im')
78
- .description('Idea Manager v2 - Brainstorming to structured tasks with prompts')
79
- .version('0.2.0');
99
+ .description('Idea Manager - Brainstorming to structured tasks with prompts')
100
+ .version(readPkgVersion());
80
101
 
81
102
  program
82
103
  .command('mcp')
@@ -135,23 +156,35 @@ program
135
156
  } catch {
136
157
  nextCli = path.join(PKG_ROOT, 'node_modules', 'next', 'dist', 'bin', 'next');
137
158
  }
159
+ if (!fs.existsSync(nextCli)) {
160
+ console.error('\n ⚠ Next.js 바이너리를 찾을 수 없습니다. 의존성이 손상된 듯합니다.');
161
+ console.error(` 재설치: npm install -g idea-manager@latest\n`);
162
+ process.exit(1);
163
+ }
138
164
 
139
- // Build if not already built
140
- const buildDir = path.join(PKG_ROOT, '.next');
141
- if (!fs.existsSync(buildDir)) {
165
+ // Build if not already built (check BUILD_ID, not just .next dir existence)
166
+ const buildMarker = path.join(PKG_ROOT, '.next', 'BUILD_ID');
167
+ if (!fs.existsSync(buildMarker)) {
142
168
  console.log('\n IM - First run: building... (this may take a minute)\n');
143
- const buildResult = spawn(process.execPath, [nextCli, 'build'], {
169
+ const buildResult = spawn(process.execPath, [nextCli, 'build', '--webpack'], {
144
170
  cwd: PKG_ROOT,
145
171
  stdio: 'inherit',
146
172
  env: { ...process.env, NODE_ENV: 'production' },
147
173
  });
148
- await new Promise<void>((resolve, reject) => {
149
- buildResult.on('exit', (code) => {
150
- if (code !== 0) reject(new Error(`Build failed with code ${code}`));
151
- else resolve();
174
+ try {
175
+ await new Promise<void>((resolve, reject) => {
176
+ buildResult.on('exit', (code) => {
177
+ if (code !== 0) reject(new Error(`Build failed with code ${code}`));
178
+ else resolve();
179
+ });
180
+ buildResult.on('error', reject);
152
181
  });
153
- buildResult.on('error', reject);
154
- });
182
+ } catch (err) {
183
+ console.error(`\n ⚠ 빌드 실패: ${err instanceof Error ? err.message : String(err)}`);
184
+ console.error(` 글로벌 설치 시 devDependencies가 빠졌을 수 있습니다.`);
185
+ console.error(` 시도: cd "${PKG_ROOT}" && npm install\n`);
186
+ process.exit(1);
187
+ }
155
188
  }
156
189
 
157
190
  console.log(`\n IM - Idea Manager`);
@@ -96,6 +96,17 @@ export default function DashboardPanel() {
96
96
  if (savedTab) setTab(savedTab);
97
97
  }, [fetchData]);
98
98
 
99
+ // Sync memo when another instance (⌘M layer) saves
100
+ useEffect(() => {
101
+ const onSync = () => {
102
+ fetch('/api/global-memo').then(r => r.ok ? r.json() : null)
103
+ .then(d => { if (d?.content !== undefined) setMemoContent(d.content); })
104
+ .catch(() => {});
105
+ };
106
+ window.addEventListener('global-memo-updated', onSync);
107
+ return () => window.removeEventListener('global-memo-updated', onSync);
108
+ }, []);
109
+
99
110
  const handleMemoChange = (value: string) => {
100
111
  setMemoContent(value);
101
112
  if (memoSaveTimer.current) clearTimeout(memoSaveTimer.current);
@@ -104,6 +115,8 @@ export default function DashboardPanel() {
104
115
  method: 'PUT',
105
116
  headers: { 'Content-Type': 'application/json' },
106
117
  body: JSON.stringify({ content: value }),
118
+ }).then(() => {
119
+ window.dispatchEvent(new Event('global-memo-updated'));
107
120
  });
108
121
  }, 500);
109
122
  };
@@ -34,10 +34,25 @@ export default function GlobalMemoLayer() {
34
34
  method: 'PUT',
35
35
  headers: { 'Content-Type': 'application/json' },
36
36
  body: JSON.stringify({ content: value }),
37
+ }).then(() => {
38
+ window.dispatchEvent(new Event('global-memo-updated'));
37
39
  }).catch(() => {});
38
40
  }, 600);
39
41
  }, []);
40
42
 
43
+ // Sync when other memo instance saves
44
+ useEffect(() => {
45
+ const onSync = () => {
46
+ if (!open) return;
47
+ fetch('/api/global-memo')
48
+ .then(r => r.ok ? r.json() : null)
49
+ .then(d => { if (d?.content !== undefined) setContent(d.content); })
50
+ .catch(() => {});
51
+ };
52
+ window.addEventListener('global-memo-updated', onSync);
53
+ return () => window.removeEventListener('global-memo-updated', onSync);
54
+ }, [open]);
55
+
41
56
  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
42
57
  const v = e.target.value;
43
58
  setContent(v);
@@ -173,7 +173,7 @@ export default function FileTreeDrawer({
173
173
  });
174
174
  };
175
175
 
176
- const dirName = rootPath.split('/').pop() || rootPath;
176
+ const dirName = rootPath.split(/[\\/]/).filter(Boolean).pop() || rootPath;
177
177
 
178
178
  return (
179
179
  <div
@@ -37,6 +37,7 @@ export function runAgent(
37
37
  cwd: options?.cwd || process.cwd(),
38
38
  stdio: ['pipe', 'pipe', 'pipe'],
39
39
  shell: process.platform === 'win32',
40
+ windowsHide: true,
40
41
  env,
41
42
  });
42
43
 
@@ -50,9 +51,26 @@ export function runAgent(
50
51
  }, options.timeoutMs);
51
52
  }
52
53
 
53
- // Write prompt to stdin and close it
54
- proc.stdin?.write(prompt);
55
- proc.stdin?.end();
54
+ // Spawn failure (e.g., binary not found) → reject instead of hanging.
55
+ proc.on('error', (err) => {
56
+ if (timeoutTimer) clearTimeout(timeoutTimer);
57
+ const code = (err as NodeJS.ErrnoException).code;
58
+ if (code === 'ENOENT') {
59
+ reject(new Error(`${config.name} CLI not found on PATH. Install it first.`));
60
+ } else {
61
+ reject(new Error(`${config.name} CLI error: ${err.message}`));
62
+ }
63
+ });
64
+
65
+ // Write prompt to stdin and close it. Wrap in try/catch for broken pipe cases.
66
+ try {
67
+ proc.stdin?.write(prompt, 'utf8');
68
+ proc.stdin?.end();
69
+ } catch (err) {
70
+ if (timeoutTimer) clearTimeout(timeoutTimer);
71
+ reject(new Error(`Failed to pipe prompt to ${config.name}: ${(err as Error).message}`));
72
+ return;
73
+ }
56
74
 
57
75
  let buffer = '';
58
76
  let resultText = '';
@@ -103,10 +121,6 @@ export function runAgent(
103
121
  stderrText += chunk.toString();
104
122
  });
105
123
 
106
- proc.on('error', (err) => {
107
- reject(new Error(`${config.name} CLI error: ${err.message}`));
108
- });
109
-
110
124
  proc.on('exit', (code, signal) => {
111
125
  if (timeoutTimer) clearTimeout(timeoutTimer);
112
126
  if (!useStreamJson && config.cleanOutput) {
@@ -1,5 +1,5 @@
1
- import { spawn } from 'child_process';
2
- import { readFileSync } from 'fs';
1
+ import { spawn, execSync } from 'child_process';
2
+ import { readFileSync, existsSync } from 'fs';
3
3
  import { join } from 'path';
4
4
 
5
5
  function readInstalledVersion(pkgRoot: string): string {
@@ -120,9 +120,28 @@ export async function maybeAutoUpdate(pkgRoot: string): Promise<AutoUpdateResult
120
120
  * replaces the old copy loaded in the current Node process. Sets
121
121
  * IM_NO_AUTO_UPDATE=1 on the child to prevent an update-respawn loop.
122
122
  */
123
+ function resolveImBin(): string | null {
124
+ // After `npm install -g`, the im wrapper lives in npm's global bin.
125
+ try {
126
+ const prefix = execSync('npm prefix -g', { encoding: 'utf-8' }).trim();
127
+ // On Windows the wrapper is `<prefix>\im.cmd`; on POSIX it's `<prefix>/bin/im`.
128
+ const candidates = process.platform === 'win32'
129
+ ? [join(prefix, 'im.cmd'), join(prefix, 'im.ps1'), join(prefix, 'im')]
130
+ : [join(prefix, 'bin', 'im')];
131
+ for (const c of candidates) {
132
+ if (existsSync(c)) return c;
133
+ }
134
+ } catch { /* fall through */ }
135
+ return null;
136
+ }
137
+
123
138
  export function respawnSelf(): void {
124
139
  const args = process.argv.slice(2);
125
- const child = spawn('im', args, {
140
+ const binPath = resolveImBin();
141
+ const cmd = binPath ?? 'im';
142
+ // On Windows, .cmd wrappers require shell:true (cmd.exe interprets them).
143
+ // On POSIX, prefer shell:false with an absolute path for reliability.
144
+ const child = spawn(cmd, args, {
126
145
  stdio: 'inherit',
127
146
  shell: process.platform === 'win32',
128
147
  env: { ...process.env, IM_NO_AUTO_UPDATE: '1' },
@@ -1,4 +1,5 @@
1
1
  import fs from 'fs';
2
+ import path from 'path';
2
3
  import { getDbPath } from '../utils/paths';
3
4
  import { initSchema } from './schema';
4
5
  import { initScheduler } from '../scheduler';
@@ -20,7 +21,17 @@ class DatabaseWrapper {
20
21
  private save() {
21
22
  if (!this.dirty) return;
22
23
  const data = this.db.export();
23
- fs.writeFileSync(this.dbPath, Buffer.from(data));
24
+ // Atomic write: write to temp file, then rename. Prevents corruption
25
+ // if the process dies mid-write.
26
+ const tmpPath = `${this.dbPath}.tmp-${process.pid}`;
27
+ try {
28
+ fs.writeFileSync(tmpPath, Buffer.from(data));
29
+ fs.renameSync(tmpPath, this.dbPath);
30
+ } catch (err) {
31
+ // Best-effort cleanup of orphan tmp
32
+ try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
33
+ throw err;
34
+ }
24
35
  this.dirty = false;
25
36
  }
26
37
 
@@ -144,12 +155,25 @@ async function initAsync(): Promise<DatabaseWrapper> {
144
155
 
145
156
  // eslint-disable-next-line @typescript-eslint/no-require-imports
146
157
  const initSqlJs = require('sql.js/dist/sql-wasm.js');
147
- // In Node, sql.js resolves the .wasm file relative to its own __dirname
148
- // (node_modules/sql.js/dist). `serverExternalPackages: ['sql.js']` keeps
149
- // the package unbundled so that resolution works.
150
- const SQL = await initSqlJs();
158
+ // Explicitly locate the .wasm next to sql-wasm.js otherwise sql.js falls
159
+ // back to `__dirname` which can be wrong when invoked from MCP stdio mode
160
+ // (cwd may differ from PKG_ROOT).
161
+ let wasmDir: string;
162
+ try {
163
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
164
+ wasmDir = path.dirname(require.resolve('sql.js/dist/sql-wasm.js'));
165
+ } catch {
166
+ wasmDir = '';
167
+ }
168
+ const SQL = await initSqlJs(wasmDir ? {
169
+ locateFile: (file: string) => path.join(wasmDir, file),
170
+ } : undefined);
151
171
 
152
172
  const dbPath = getDbPath();
173
+ // Ensure parent directory exists (first-run safety on fresh installs).
174
+ const dbDir = path.dirname(dbPath);
175
+ if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
176
+
153
177
  let db;
154
178
  if (fs.existsSync(dbPath)) {
155
179
  const fileBuffer = fs.readFileSync(dbPath);
@@ -10,7 +10,12 @@ function formatDate(d: Date): string {
10
10
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
11
11
  }
12
12
 
13
- function sendMacNotification(title: string, message: string) {
13
+ function sendNotification(title: string, message: string) {
14
+ // macOS native notification via osascript. Other platforms get a console log.
15
+ if (process.platform !== 'darwin') {
16
+ console.log(`[${title}] ${message}`);
17
+ return;
18
+ }
14
19
  const escaped = message.replace(/"/g, '\\"').replace(/\n/g, '\\n');
15
20
  const script = `display notification "${escaped}" with title "${title}"`;
16
21
  exec(`osascript -e '${script}'`, (err) => {
@@ -60,7 +65,7 @@ async function checkMorningNotification() {
60
65
  if (activeTasks.count > 0) lines.push(`진행 중: ${activeTasks.count}개`);
61
66
  if (problemTasks.count > 0) lines.push(`문제: ${problemTasks.count}개`);
62
67
 
63
- sendMacNotification('IM - 오늘의 할 일', lines.join('\n'));
68
+ sendNotification('IM - 오늘의 할 일', lines.join('\n'));
64
69
  lastNotifiedDate = today;
65
70
  } catch (err) {
66
71
  console.error('[Scheduler] error:', err);
@@ -71,8 +76,9 @@ export function initScheduler() {
71
76
  if (initialized) return;
72
77
  initialized = true;
73
78
 
74
- // Check every minute
79
+ // Check every minute. unref() so it doesn't pin the event loop for MCP stdio mode.
75
80
  timer = setInterval(checkMorningNotification, 60 * 1000);
81
+ timer.unref?.();
76
82
 
77
83
  // Also check immediately on startup
78
84
  checkMorningNotification();
@@ -2,24 +2,26 @@ import { execFile } from 'child_process';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
 
5
- function quoteArg(arg: string): string {
6
- // Quote args with spaces for shell mode
7
- if (!arg.includes(' ') && !arg.includes('"')) return arg;
8
- if (process.platform === 'win32') {
9
- // cmd.exe uses "" to escape quotes
10
- return `"${arg.replace(/"/g, '""')}"`;
11
- }
12
- return `"${arg.replace(/"/g, '\\"')}"`;
13
- }
5
+ // Longer timeout for network-heavy operations (clone/push/pull over slow links).
6
+ const NETWORK_CMDS = new Set(['clone', 'push', 'pull', 'fetch']);
14
7
 
15
8
  function exec(cmd: string, args: string[], cwd?: string): Promise<string> {
16
9
  return new Promise((resolve, reject) => {
17
- const quotedArgs = args.map(quoteArg);
18
- execFile(cmd, quotedArgs, { cwd, shell: true, timeout: 60000 }, (err, stdout, stderr) => {
10
+ const isNetwork = args.some(a => NETWORK_CMDS.has(a));
11
+ const timeout = isNetwork ? 300000 : 30000; // 5min / 30s
12
+ // shell: false — Node handles arg escaping natively on all platforms.
13
+ // On Windows, execFile still resolves `.cmd`/`.bat` via PATHEXT for known binaries.
14
+ execFile(cmd, args, {
15
+ cwd,
16
+ timeout,
17
+ shell: process.platform === 'win32',
18
+ windowsHide: true,
19
+ maxBuffer: 10 * 1024 * 1024,
20
+ }, (err, stdout, stderr) => {
19
21
  if (err) {
20
- reject(new Error(stderr?.trim() || err.message));
22
+ reject(new Error(stderr?.toString().trim() || err.message));
21
23
  } else {
22
- resolve(stdout.trim());
24
+ resolve(stdout.toString().trim());
23
25
  }
24
26
  });
25
27
  });
@@ -43,7 +45,8 @@ export function isGitRepo(dir: string): boolean {
43
45
 
44
46
  export async function ghCreateRepo(name: string): Promise<string> {
45
47
  // Create private repo and get URL
46
- const result = await exec('gh', ['repo', 'create', name, '--private', '--confirm']);
48
+ // --confirm was removed in gh 2.x; repo is created non-interactively by default with name+flag
49
+ const result = await exec('gh', ['repo', 'create', name, '--private']);
47
50
  // Extract repo URL from output
48
51
  const urlMatch = result.match(/https:\/\/github\.com\/\S+/);
49
52
  if (urlMatch) return urlMatch[0];
@@ -1,5 +1,5 @@
1
1
  import fs from 'fs';
2
- import { ensureDb } from './db';
2
+ import { ensureDb, getDb } from './db';
3
3
  import { runAgent } from './ai/client';
4
4
  import { listProjects, getProject } from './db/queries/projects';
5
5
  import { getSubProject } from './db/queries/sub-projects';
@@ -101,7 +101,7 @@ async function executeTask(task: ITask, project: IProject, options: WatcherOptio
101
101
  const content = `[진행 중]\n${accumulated}`;
102
102
  if (progressMsgId) {
103
103
  // Update existing progress message
104
- const { getDb } = require('./db/index');
104
+ // getDb imported at top
105
105
  const db = getDb();
106
106
  db.prepare('UPDATE task_conversations SET content = ? WHERE id = ?').run(content, progressMsgId);
107
107
  } else {
@@ -135,7 +135,7 @@ async function executeTask(task: ITask, project: IProject, options: WatcherOptio
135
135
 
136
136
  // Replace progress message with final result
137
137
  if (progressMsgId) {
138
- const { getDb } = require('./db/index');
138
+ // getDb imported at top
139
139
  const db = getDb();
140
140
  db.prepare('UPDATE task_conversations SET content = ? WHERE id = ?').run(result || '(no output)', progressMsgId);
141
141
  } else {
@@ -152,7 +152,7 @@ async function executeTask(task: ITask, project: IProject, options: WatcherOptio
152
152
 
153
153
  // Replace progress message with error
154
154
  if (progressMsgId) {
155
- const { getDb } = require('./db/index');
155
+ // getDb imported at top
156
156
  const db = getDb();
157
157
  db.prepare('UPDATE task_conversations SET content = ? WHERE id = ?').run(`[error] ${errorMsg}`, progressMsgId);
158
158
  } else {