create-walle 0.9.7 → 0.9.9

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 (146) hide show
  1. package/README.md +1 -1
  2. package/bin/create-walle.js +32 -1
  3. package/bin/mcp-inject.js +60 -0
  4. package/package.json +11 -3
  5. package/template/bin/setup.js +2 -1
  6. package/template/claude-code-skill.md +35 -34
  7. package/template/claude-task-manager/api-prompts.js +113 -70
  8. package/template/claude-task-manager/approval-agent.js +127 -22
  9. package/template/claude-task-manager/atomic-write.js +22 -0
  10. package/template/claude-task-manager/db.js +515 -28
  11. package/template/claude-task-manager/git-utils.js +112 -1
  12. package/template/claude-task-manager/public/css/setup.css +191 -0
  13. package/template/claude-task-manager/public/css/walle-session.css +3 -0
  14. package/template/claude-task-manager/public/css/walle.css +305 -0
  15. package/template/claude-task-manager/public/index.html +3174 -296
  16. package/template/claude-task-manager/public/js/setup.js +698 -0
  17. package/template/claude-task-manager/public/js/walle-session.js +124 -1
  18. package/template/claude-task-manager/public/js/walle.js +1420 -42
  19. package/template/claude-task-manager/public/setup.html +693 -104
  20. package/template/claude-task-manager/server-state.js +8 -1
  21. package/template/claude-task-manager/server.js +2280 -227
  22. package/template/claude-task-manager/session-utils.js +50 -3
  23. package/template/claude-task-manager/workers/harvest-worker.js +36 -0
  24. package/template/claude-task-manager/workers/scrollback-worker.js +60 -0
  25. package/template/claude-task-manager/workers/vterm-worker.js +179 -0
  26. package/template/package.json +2 -2
  27. package/template/wall-e/agent.js +288 -146
  28. package/template/wall-e/api-walle.js +588 -4
  29. package/template/wall-e/brain.js +565 -10
  30. package/template/wall-e/chat.js +298 -51
  31. package/template/wall-e/coding-orchestrator.js +31 -0
  32. package/template/wall-e/context/compactor.js +21 -0
  33. package/template/wall-e/context/context-builder.js +216 -64
  34. package/template/wall-e/context/topic-matcher.js +40 -11
  35. package/template/wall-e/docs/prompt-architecture.md +121 -0
  36. package/template/wall-e/embeddings.js +810 -0
  37. package/template/wall-e/eval/aggregator.js +115 -0
  38. package/template/wall-e/eval/benchmarks/chat-eval.json +1041 -0
  39. package/template/wall-e/eval/benchmarks/chat.json +82 -0
  40. package/template/wall-e/eval/benchmarks/coding.json +122 -0
  41. package/template/wall-e/eval/benchmarks/memory-retrieval.json +82 -0
  42. package/template/wall-e/eval/benchmarks/reasoning.json +82 -0
  43. package/template/wall-e/eval/benchmarks.js +464 -0
  44. package/template/wall-e/eval/chat-eval.js +509 -0
  45. package/template/wall-e/eval/evaluate.js +202 -0
  46. package/template/wall-e/eval/evaluator.js +373 -0
  47. package/template/wall-e/eval/exporter.js +212 -0
  48. package/template/wall-e/eval/harvester.js +472 -0
  49. package/template/wall-e/eval/head-to-head.js +337 -0
  50. package/template/wall-e/eval/promoter.js +146 -0
  51. package/template/wall-e/eval/replay.js +381 -0
  52. package/template/wall-e/eval/shadow.js +144 -0
  53. package/template/wall-e/eval/train.py +320 -0
  54. package/template/wall-e/eval/trainer.js +232 -0
  55. package/template/wall-e/evaluation/complexity.js +3 -0
  56. package/template/wall-e/evaluation/index.js +1 -1
  57. package/template/wall-e/evaluation/learner.js +14 -2
  58. package/template/wall-e/evaluation/quorum-evaluator.js +544 -0
  59. package/template/wall-e/evaluation/router.js +237 -29
  60. package/template/wall-e/evaluation/scorecard.js +74 -2
  61. package/template/wall-e/evaluation/self-critique.js +188 -0
  62. package/template/wall-e/evaluation/tier-selector.js +166 -0
  63. package/template/wall-e/evaluation/user-signals.js +109 -0
  64. package/template/wall-e/extraction/knowledge-extractor.js +60 -17
  65. package/template/wall-e/lib/scheduler.js +389 -0
  66. package/template/wall-e/llm/client.js +74 -6
  67. package/template/wall-e/llm/index.js +1 -0
  68. package/template/wall-e/llm/mlx.js +220 -0
  69. package/template/wall-e/llm/ollama-setup.js +228 -0
  70. package/template/wall-e/llm/ollama.js +20 -3
  71. package/template/wall-e/llm/provider-availability.js +213 -0
  72. package/template/wall-e/llm/provider-detector.js +273 -0
  73. package/template/wall-e/loops/backfill.js +338 -0
  74. package/template/wall-e/loops/initiative.js +13 -0
  75. package/template/wall-e/loops/reflect.js +13 -0
  76. package/template/wall-e/loops/tasks.js +56 -11
  77. package/template/wall-e/loops/think.js +55 -4
  78. package/template/wall-e/mcp-server.js +235 -0
  79. package/template/wall-e/package.json +1 -0
  80. package/template/wall-e/scripts/eval-embeddings.js +311 -0
  81. package/template/wall-e/scripts/slack-channel-history.js +1 -1
  82. package/template/wall-e/server.js +17 -0
  83. package/template/wall-e/skills/_bundled/coding-agent/run.js +9 -1
  84. package/template/wall-e/skills/_bundled/email-sync/run.js +9 -1
  85. package/template/wall-e/skills/_bundled/file-ingest/run.js +6 -1
  86. package/template/wall-e/skills/_bundled/glean-team-sync/run.js +9 -4
  87. package/template/wall-e/skills/_bundled/google-calendar/run.js +40 -6
  88. package/template/wall-e/skills/_bundled/mcp-scan/run.js +9 -4
  89. package/template/wall-e/skills/_bundled/model-trainer/SKILL.md +1 -1
  90. package/template/wall-e/skills/_bundled/morning-briefing/run.js +82 -6
  91. package/template/wall-e/skills/_bundled/proactive-alerts/run.js +9 -4
  92. package/template/wall-e/skills/_bundled/slack-mentions/run.js +24 -17
  93. package/template/wall-e/skills/_bundled/training-harvester/SKILL.md +43 -0
  94. package/template/wall-e/skills/_bundled/training-harvester/run.js +46 -0
  95. package/template/wall-e/skills/skill-planner.js +92 -64
  96. package/template/wall-e/telemetry.js +65 -1
  97. package/template/wall-e/test/eval/aggregator.test.js +180 -0
  98. package/template/wall-e/test/eval/benchmarks.test.js +373 -0
  99. package/template/wall-e/test/eval/brain-shadow.test.js +359 -0
  100. package/template/wall-e/test/eval/evaluate.test.js +221 -0
  101. package/template/wall-e/test/eval/evaluator.test.js +340 -0
  102. package/template/wall-e/test/eval/exporter.test.js +353 -0
  103. package/template/wall-e/test/eval/harvester.test.js +718 -0
  104. package/template/wall-e/test/eval/head-to-head.test.js +485 -0
  105. package/template/wall-e/test/eval/promoter.test.js +175 -0
  106. package/template/wall-e/test/eval/shadow.test.js +156 -0
  107. package/template/wall-e/test/eval/trainer.test.js +314 -0
  108. package/template/wall-e/test/evaluation/scorecard.test.js +33 -0
  109. package/template/wall-e/test/llm/client.test.js +74 -6
  110. package/template/wall-e/test/training/aggregator.test.js +180 -0
  111. package/template/wall-e/test/training/brain-shadow.test.js +359 -0
  112. package/template/wall-e/test/training/evaluator.test.js +340 -0
  113. package/template/wall-e/test/training/exporter.test.js +141 -1
  114. package/template/wall-e/test/training/harvester.test.js +718 -0
  115. package/template/wall-e/test/training/promoter.test.js +175 -0
  116. package/template/wall-e/test/training/shadow.test.js +156 -0
  117. package/template/wall-e/test/training/trainer.test.js +77 -4
  118. package/template/wall-e/tests/backfill.test.js +126 -0
  119. package/template/wall-e/tests/brain.test.js +4 -4
  120. package/template/wall-e/tests/coding-orchestrator.test.js +212 -217
  121. package/template/wall-e/tests/context-builder.test.js +42 -35
  122. package/template/wall-e/tests/embeddings.test.js +378 -0
  123. package/template/wall-e/tests/mcp-inject.test.js +68 -0
  124. package/template/wall-e/tests/mcp-server.test.js +219 -0
  125. package/template/wall-e/tests/ollama-setup.test.js +81 -0
  126. package/template/wall-e/tests/provider-availability.test.js +231 -0
  127. package/template/wall-e/tests/provider-detector.test.js +127 -0
  128. package/template/wall-e/tests/quorum-evaluator.test.js +277 -0
  129. package/template/wall-e/tests/scheduler.test.js +546 -0
  130. package/template/wall-e/tests/scorecard-evolution.test.js +96 -0
  131. package/template/wall-e/tests/self-critique.test.js +162 -0
  132. package/template/wall-e/tests/think.test.js +34 -15
  133. package/template/wall-e/tests/tier-selector.test.js +109 -0
  134. package/template/wall-e/tests/user-signals.test.js +73 -0
  135. package/template/wall-e/tools/local-tools.js +143 -14
  136. package/template/wall-e/tools/slack-mcp.js +20 -16
  137. package/template/wall-e/training/aggregator.js +115 -0
  138. package/template/wall-e/training/evaluator.js +373 -0
  139. package/template/wall-e/training/exporter.js +86 -1
  140. package/template/wall-e/training/harvester.js +476 -0
  141. package/template/wall-e/training/promoter.js +146 -0
  142. package/template/wall-e/training/replay.js +381 -0
  143. package/template/wall-e/training/shadow.js +144 -0
  144. package/template/wall-e/training/train.py +85 -34
  145. package/template/wall-e/training/trainer.js +35 -5
  146. package/template/wall-e/evaluation/quorum.js +0 -256
package/README.md CHANGED
@@ -23,7 +23,7 @@ An always-on AI agent that learns from your Slack, email, calendar, and coding s
23
23
  - **Second Brain** — Automatically ingests your digital life into a searchable memory store with full-text search, knowledge extraction, and pattern detection
24
24
  - **Proactive Intelligence** — Surfaces time-sensitive items, suggests actions, and delivers morning briefings and weekly reflections without being asked
25
25
  - **Chat with Tools** — Talk to Wall-E in the browser — it can search your memories, look up people, run skills, and call external tools via MCP (Slack, Glean, etc.)
26
- - **16 Bundled Skills** — Morning briefing, weekly reflection, proactive alerts, Slack monitoring, email sync, calendar integration, coding agent, model training, and more
26
+ - **17 Bundled Skills** — Morning briefing, weekly reflection, proactive alerts, Slack monitoring, email sync, calendar integration, coding agent, model evaluation, and more
27
27
  - **Multi-Model** — Works with Claude, GPT, Gemini, and local models via Ollama with smart routing
28
28
  - **Skill Management GUI** — Search, filter, create, edit, and monitor skills from the browser with rich cards, config forms, execution history, export/import, and pre-flight validation
29
29
  - **Multi-Device** — Share your brain across machines via Dropbox or iCloud
@@ -13,6 +13,7 @@ const RED = '\x1b[31m';
13
13
  const CYAN = '\x1b[36m';
14
14
  const RESET = '\x1b[0m';
15
15
 
16
+ const { injectMcpConfigs } = require('./mcp-inject');
16
17
  const TEMPLATE_DIR = path.join(__dirname, '..', 'template');
17
18
  const LABEL = 'com.walle.server';
18
19
  const INSTALL_PATH_FILE = path.join(process.env.HOME, '.walle', 'install-path');
@@ -70,6 +71,29 @@ ${BOLD}${CYAN} Wall-E${RESET} — Your personal digital twin
70
71
  `);
71
72
  }
72
73
 
74
+ function printMcpResults(wallePort) {
75
+ try {
76
+ const results = injectMcpConfigs(wallePort);
77
+ const hasAny = results.some(r => r.action !== 'not_installed');
78
+ if (!hasAny) return;
79
+
80
+ console.log(` ${BOLD}MCP Integrations:${RESET}`);
81
+ for (const r of results) {
82
+ if (r.action === 'not_installed') {
83
+ console.log(` ${DIM}- ${r.tool} -- not installed${RESET}`);
84
+ } else if (r.action === 'already_configured') {
85
+ console.log(` ${DIM}= ${r.tool} -- already configured${RESET}`);
86
+ } else if (r.action === 'updated') {
87
+ console.log(` ${GREEN}~ ${r.tool}${RESET} -- updated port in ${DIM}${r.configPath}${RESET}`);
88
+ } else {
89
+ console.log(` ${GREEN}+ ${r.tool}${RESET} -- added Wall-E to ${DIM}${r.configPath}${RESET}`);
90
+ }
91
+ }
92
+ console.log(`\n ${DIM}Your AI coding tools can now access Wall-E's memory and knowledge.`);
93
+ console.log(` To undo: remove the "wall-e" entry from the config files above.${RESET}\n`);
94
+ } catch {}
95
+ }
96
+
73
97
  function install(targetDir) {
74
98
  // If no target dir given, check for existing install
75
99
  if (!targetDir) {
@@ -128,6 +152,10 @@ function install(targetDir) {
128
152
  `WALLE_OWNER_NAME=${ownerName}`,
129
153
  '# ANTHROPIC_API_KEY=',
130
154
  '',
155
+ '# AI Provider: anthropic, openai, google, ollama',
156
+ '# WALLE_PROVIDER=anthropic',
157
+ '# WALLE_MODEL=',
158
+ '',
131
159
  `CTM_PORT=${port}`,
132
160
  `WALL_E_PORT=${wallePort}`,
133
161
  '',
@@ -172,6 +200,7 @@ function install(targetDir) {
172
200
  npx create-walle status ${DIM}Check status${RESET}
173
201
  npx create-walle logs ${DIM}View logs${RESET}
174
202
  `);
203
+ printMcpResults(parseInt(wallePort));
175
204
  }
176
205
 
177
206
  function update() {
@@ -218,7 +247,7 @@ function update() {
218
247
  console.log(` Installing dependencies...\n`);
219
248
  npmInstall(dir);
220
249
 
221
- // 6. Start again
250
+ // 7. Start again
222
251
  console.log(`\n Starting Wall-E...`);
223
252
  startForegroundOrService(dir, port);
224
253
 
@@ -227,6 +256,7 @@ function update() {
227
256
 
228
257
  ${DIM}Your .env and config were preserved.${RESET}
229
258
  `);
259
+ printMcpResults(parseInt(readWallePort(dir)));
230
260
  }
231
261
 
232
262
  function start() {
@@ -235,6 +265,7 @@ function start() {
235
265
  console.log(`\n Starting Wall-E from ${DIM}${dir}${RESET} on port ${port}...`);
236
266
  installService(dir, port);
237
267
  console.log(` ${GREEN}Running!${RESET} http://localhost:${port}\n`);
268
+ printMcpResults(parseInt(readWallePort(dir)));
238
269
  }
239
270
 
240
271
  function stop() {
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const MCP_TARGETS = [
6
+ { tool: 'Claude Code', configPath: '.claude/mcp.json', detectDir: '.claude' },
7
+ { tool: 'Cursor', configPath: '.cursor/mcp.json', detectDir: '.cursor' },
8
+ { tool: 'Windsurf', configPath: '.codeium/windsurf/mcp_config.json', detectDir: '.codeium/windsurf' },
9
+ { tool: 'Claude Desktop', configPath: 'Library/Application Support/Claude/claude_desktop_config.json', detectDir: 'Library/Application Support/Claude' },
10
+ ];
11
+
12
+ /**
13
+ * Inject Wall-E MCP server config into detected AI tool config files.
14
+ * @param {number} wallePort - The Wall-E HTTP port
15
+ * @param {string} [homeDir] - Home directory override for testing
16
+ * @returns {Array<{tool: string, action: string, configPath?: string}>}
17
+ */
18
+ function injectMcpConfigs(wallePort, homeDir) {
19
+ const home = homeDir || process.env.HOME;
20
+ const walleUrl = `http://localhost:${wallePort}/mcp`;
21
+ const results = [];
22
+
23
+ for (const target of MCP_TARGETS) {
24
+ const detectPath = path.join(home, target.detectDir);
25
+ const configPath = path.join(home, target.configPath);
26
+
27
+ if (!fs.existsSync(detectPath)) {
28
+ results.push({ tool: target.tool, action: 'not_installed' });
29
+ continue;
30
+ }
31
+
32
+ let config = {};
33
+ if (fs.existsSync(configPath)) {
34
+ try {
35
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
36
+ } catch {
37
+ config = {};
38
+ }
39
+ }
40
+
41
+ if (!config.mcpServers) config.mcpServers = {};
42
+
43
+ const existing = config.mcpServers['wall-e'];
44
+ if (existing && existing.url === walleUrl) {
45
+ results.push({ tool: target.tool, action: 'already_configured', configPath });
46
+ continue;
47
+ }
48
+
49
+ const action = existing ? 'updated' : 'added';
50
+ config.mcpServers['wall-e'] = { type: 'http', url: walleUrl };
51
+
52
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
53
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
54
+ results.push({ tool: target.tool, action, configPath });
55
+ }
56
+
57
+ return results;
58
+ }
59
+
60
+ module.exports = { injectMcpConfigs, MCP_TARGETS };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-walle",
3
- "version": "0.9.7",
4
- "description": "CTM + Wall-E AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini & Aider, plus prompt editor, task queue, and an agent that learns from Slack, email & calendar.",
3
+ "version": "0.9.9",
4
+ "description": "CTM + Wall-E \u2014 AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini & Aider, plus prompt editor, task queue, and an agent that learns from Slack, email & calendar.",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"
7
7
  },
@@ -15,5 +15,13 @@
15
15
  "prepublishOnly": "bash build.sh"
16
16
  },
17
17
  "license": "MIT",
18
- "keywords": ["ctm", "task-manager", "wall-e", "ai", "digital-twin", "claude", "personal-assistant"]
18
+ "keywords": [
19
+ "ctm",
20
+ "task-manager",
21
+ "wall-e",
22
+ "ai",
23
+ "digital-twin",
24
+ "claude",
25
+ "personal-assistant"
26
+ ]
19
27
  }
@@ -7,6 +7,7 @@
7
7
  const { execFileSync } = require('child_process');
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
+ const { atomicWriteFileSync } = require('../claude-task-manager/atomic-write');
10
11
 
11
12
  const ROOT = path.resolve(__dirname, '..');
12
13
  const ENV_PATH = path.join(ROOT, '.env');
@@ -41,7 +42,7 @@ function runIfNeeded() {
41
42
  `WALLE_OWNER_NAME=${ownerName}`,
42
43
  '# ANTHROPIC_API_KEY=sk-ant-...',
43
44
  ];
44
- fs.writeFileSync(ENV_PATH, envLines.join('\n') + '\n', { mode: 0o600 });
45
+ atomicWriteFileSync(ENV_PATH, envLines.join('\n') + '\n', { mode: 0o600 });
45
46
 
46
47
  // Set in current process
47
48
  process.env.WALLE_OWNER_NAME = ownerName;
@@ -1,10 +1,22 @@
1
- # Wall-E Claude Code Skill
1
+ # Wall-E -- Claude Code MCP Server
2
2
 
3
- Wall-E can be used as an MCP server from Claude Code, giving Claude access to your personal knowledge base, calendar, and memory search.
3
+ Wall-E automatically registers as an MCP server in Claude Code, Cursor, Windsurf, and Claude Desktop during installation.
4
4
 
5
- ## Install as MCP Server
5
+ ## Automatic Setup
6
6
 
7
- Add to your Claude Code MCP config (`~/.claude/mcp.json`):
7
+ When you install or start Wall-E (`npx create-walle install ./my-agent`), it detects installed AI coding tools and adds itself to their MCP config. You'll see output like:
8
+
9
+ ```
10
+ MCP Integrations:
11
+ + Claude Code -- added Wall-E to ~/.claude/mcp.json
12
+ + Cursor -- added Wall-E to ~/.cursor/mcp.json
13
+ ```
14
+
15
+ No manual configuration needed.
16
+
17
+ ## Manual Setup
18
+
19
+ If auto-detection missed your tool, add this to your MCP config:
8
20
 
9
21
  ```json
10
22
  {
@@ -17,44 +29,33 @@ Add to your Claude Code MCP config (`~/.claude/mcp.json`):
17
29
  }
18
30
  ```
19
31
 
20
- Then start Wall-E's standalone HTTP server:
21
-
22
- ```bash
23
- cd wall-e
24
- WALL_E_PORT=3457 node agent.js
25
- ```
32
+ Config file locations:
33
+ - Claude Code: `~/.claude/mcp.json`
34
+ - Cursor: `~/.cursor/mcp.json`
35
+ - Windsurf: `~/.codeium/windsurf/mcp_config.json`
36
+ - Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json`
26
37
 
27
38
  ## Available Tools
28
39
 
29
- Once connected, Claude Code gains access to:
40
+ Once connected, your AI coding tool gains access to:
30
41
 
31
42
  | Tool | Description |
32
43
  |---|---|
33
44
  | `search_memories` | Full-text search across all ingested memories (Slack, email, calendar) |
34
- | `calendar_events` | Get upcoming calendar events with attendees, location, notes |
35
- | `remember_fact` | Store new knowledge in the brain |
36
- | `run_skill` | Execute any bundled or custom skill |
37
- | `list_mcp_tools` | Discover tools from Wall-E's own MCP connections |
38
-
39
- ## Use Cases in Claude Code
45
+ | `remember` | Store new knowledge in the brain |
46
+ | `list_knowledge` | Browse stored knowledge |
47
+ | `list_people` | List known people with relationships |
48
+ | `get_brief` | Get today's daily briefing |
49
+ | `ask_walle` | Ask Wall-E a question (uses full chat pipeline with memory context) |
40
50
 
41
- Once Wall-E is connected as an MCP server, you can ask Claude Code:
51
+ ## Use Cases
42
52
 
43
- - "What meetings do I have tomorrow?" searches calendar memories
44
- - "What did I discuss with Alice last week?" searches Slack memories
45
- - "Remember that I prefer Tailwind over Bootstrap" stores knowledge
46
- - "Run the morning briefing skill" generates a daily summary
47
- - "Search my memories for the database migration discussion" — full-text search
53
+ - "What meetings do I have tomorrow?" -- searches calendar memories
54
+ - "What did I discuss with Alice last week?" -- searches Slack memories
55
+ - "Remember that I prefer Tailwind over Bootstrap" -- stores knowledge
56
+ - "Who do I know at Acme Corp?" -- searches people
57
+ - "Give me my daily brief" -- generates a summary
48
58
 
49
- ## CLAUDE.md Integration
59
+ ## To Undo
50
60
 
51
- Add to your project's `CLAUDE.md` to give Claude Code context about Wall-E:
52
-
53
- ```markdown
54
- ## Wall-E (Personal Digital Twin)
55
-
56
- This project has Wall-E connected as an MCP server. Use it to:
57
- - Search the owner's memories and knowledge base
58
- - Check calendar events and meeting schedules
59
- - Look up past Slack conversations for context
60
- ```
61
+ Remove the `"wall-e"` entry from the config files listed above.
@@ -638,75 +638,114 @@ function handleListConversations(req, res, url) {
638
638
  jsonResponse(res, 200, db.listSessionConversations(opts));
639
639
  }
640
640
 
641
+ // Core import logic shared by API handler and auto-import
642
+ const { getAllSessionFiles, getAllSessionFilesAsync, parseSessionFile } = require('./session-utils');
643
+ const fsp = require('fs').promises;
644
+
645
+ function importSessionFile(filePath, projectPath, projectEntry) {
646
+ const parsed = parseSessionFile(filePath, projectPath, projectEntry);
647
+ if (parsed.isEmpty) return false;
648
+
649
+ const existing = db.getSessionConversation(parsed.sessionId);
650
+ // Skip re-import if file size unchanged AND model already populated
651
+ if (existing && existing.file_size === parsed.fileSize && existing.model_provider) return false;
652
+
653
+ const content = fs.readFileSync(filePath, 'utf8');
654
+ const lines = content.split('\n').filter(Boolean);
655
+ const messages = [];
656
+ let assistantCount = 0;
657
+
658
+ for (const line of lines) {
659
+ try {
660
+ const entry = JSON.parse(line);
661
+ if (entry.type === 'user' && entry.message?.role === 'user') {
662
+ const c = entry.message.content;
663
+ const text = typeof c === 'string' ? c
664
+ : Array.isArray(c) ? c.filter(b => b.type === 'text').map(b => b.text).join('\n') : '';
665
+ if (text) messages.push({ role: 'user', text: text.slice(0, 5000), timestamp: entry.timestamp });
666
+ } else if (entry.type === 'assistant' && entry.message?.role === 'assistant') {
667
+ const c = entry.message.content;
668
+ if (!Array.isArray(c)) continue;
669
+ const parts = [];
670
+ for (const block of c) {
671
+ if (block.type === 'text' && block.text) parts.push(block.text);
672
+ else if (block.type === 'tool_use') parts.push(`[Tool: ${block.name}]`);
673
+ }
674
+ if (parts.length > 0) {
675
+ const lastMsg = messages[messages.length - 1];
676
+ if (lastMsg && lastMsg.role === 'assistant' && lastMsg._parent === entry.parentUuid) {
677
+ lastMsg.text = parts.join('\n').slice(0, 5000);
678
+ } else {
679
+ messages.push({ role: 'assistant', text: parts.join('\n').slice(0, 5000), timestamp: entry.timestamp, _parent: entry.parentUuid });
680
+ assistantCount++;
681
+ }
682
+ }
683
+ }
684
+ } catch {}
685
+ }
686
+ messages.forEach(m => delete m._parent);
687
+
688
+ db.importSessionConversation({
689
+ session_id: parsed.sessionId,
690
+ project_path: parsed.project,
691
+ messages,
692
+ user_msg_count: parsed.userMsgCount,
693
+ assistant_msg_count: assistantCount,
694
+ title: parsed.title,
695
+ first_message: parsed.firstMessage,
696
+ git_branch: parsed.gitBranch,
697
+ file_size: parsed.fileSize,
698
+ session_created_at: parsed.timestamp,
699
+ hostname: parsed.hostname,
700
+ model_provider: parsed.modelProvider,
701
+ model_id: parsed.modelId,
702
+ });
703
+ return true;
704
+ }
705
+
641
706
  async function handleImportConversations(req, res) {
642
- // Import all sessions from ~/.claude/projects/ into the DB
643
707
  try {
644
- const { getAllSessionFiles, parseSessionFile } = require('./session-utils');
645
708
  let imported = 0;
646
- const allFiles = getAllSessionFiles();
709
+ for (const { filePath, projectPath, projectEntry } of getAllSessionFiles()) {
710
+ try { if (importSessionFile(filePath, projectPath, projectEntry)) imported++; } catch {}
711
+ }
712
+ jsonResponse(res, 200, { ok: true, imported });
713
+ } catch (e) { jsonResponse(res, 500, { error: e.message }); }
714
+ }
715
+
716
+ // Incremental auto-import: only processes files modified since last scan
717
+ // Uses async filesystem operations to avoid blocking the event loop
718
+ // (critical when ~/.claude/projects is on Dropbox/network-synced FS)
719
+ async function runIncrementalConversationImport() {
720
+ try {
721
+ // One-time backfill: force full rescan to populate model_provider for existing sessions
722
+ if (!db.getSetting('conversation_model_backfill_done', false)) {
723
+ db.setSetting('conversation_import_last_scan_at', 0);
724
+ db.setSetting('conversation_model_backfill_done', true);
725
+ console.log('[auto-import] One-time model backfill: forcing full rescan');
726
+ }
727
+ const lastScanAt = db.getSetting('conversation_import_last_scan_at', 0);
728
+ const scanStart = Date.now();
729
+ const allFiles = await getAllSessionFilesAsync();
730
+ let imported = 0;
731
+ let scanned = 0;
647
732
 
648
733
  for (const { filePath, projectPath, projectEntry } of allFiles) {
649
734
  try {
650
- const parsed = parseSessionFile(filePath, projectPath, projectEntry);
651
- if (parsed.isEmpty) continue;
652
-
653
- const existing = db.getSessionConversation(parsed.sessionId);
654
- if (existing && existing.file_size === parsed.fileSize) continue;
655
-
656
- // Read full messages
657
- const content = fs.readFileSync(filePath, 'utf8');
658
- const lines = content.split('\n').filter(Boolean);
659
- const messages = [];
660
- let assistantCount = 0;
661
-
662
- for (const line of lines) {
663
- try {
664
- const entry = JSON.parse(line);
665
- if (entry.type === 'user' && entry.message?.role === 'user') {
666
- const c = entry.message.content;
667
- const text = typeof c === 'string' ? c
668
- : Array.isArray(c) ? c.filter(b => b.type === 'text').map(b => b.text).join('\n') : '';
669
- if (text) messages.push({ role: 'user', text: text.slice(0, 5000), timestamp: entry.timestamp });
670
- } else if (entry.type === 'assistant' && entry.message?.role === 'assistant') {
671
- const c = entry.message.content;
672
- if (!Array.isArray(c)) continue;
673
- const parts = [];
674
- for (const block of c) {
675
- if (block.type === 'text' && block.text) parts.push(block.text);
676
- else if (block.type === 'tool_use') parts.push(`[Tool: ${block.name}]`);
677
- }
678
- if (parts.length > 0) {
679
- const lastMsg = messages[messages.length - 1];
680
- if (lastMsg && lastMsg.role === 'assistant' && lastMsg._parent === entry.parentUuid) {
681
- lastMsg.text = parts.join('\n').slice(0, 5000);
682
- } else {
683
- messages.push({ role: 'assistant', text: parts.join('\n').slice(0, 5000), timestamp: entry.timestamp, _parent: entry.parentUuid });
684
- assistantCount++;
685
- }
686
- }
687
- }
688
- } catch {}
689
- }
690
- messages.forEach(m => delete m._parent);
691
-
692
- db.importSessionConversation({
693
- session_id: parsed.sessionId,
694
- project_path: parsed.project,
695
- messages,
696
- user_msg_count: parsed.userMsgCount,
697
- assistant_msg_count: assistantCount,
698
- title: parsed.title,
699
- first_message: parsed.firstMessage,
700
- git_branch: parsed.gitBranch,
701
- file_size: parsed.fileSize,
702
- session_created_at: parsed.timestamp,
703
- });
704
- imported++;
735
+ const stat = await fsp.stat(filePath);
736
+ if (stat.mtimeMs <= lastScanAt) continue;
737
+ scanned++;
738
+ if (importSessionFile(filePath, projectPath, projectEntry)) imported++;
705
739
  } catch {}
706
740
  }
707
741
 
708
- jsonResponse(res, 200, { ok: true, imported });
709
- } catch (e) { jsonResponse(res, 500, { error: e.message }); }
742
+ db.setSetting('conversation_import_last_scan_at', scanStart);
743
+ if (imported > 0) console.log(`[auto-import] Imported ${imported} session conversations (scanned ${scanned}/${allFiles.length})`);
744
+ return { imported, scanned, total: allFiles.length };
745
+ } catch (e) {
746
+ console.error('[auto-import] Error:', e.message);
747
+ return { imported: 0, scanned: 0, total: 0 };
748
+ }
710
749
  }
711
750
 
712
751
  function handleGetConversation(req, res, url) {
@@ -1057,7 +1096,7 @@ async function handlePermAISuggest(req, res) {
1057
1096
  }
1058
1097
 
1059
1098
  // Use LLM to interpret the natural language permission request
1060
- const result = await classifyPermissionIntent(body.query || '');
1099
+ const result = await classifyPermissionIntent(query);
1061
1100
  jsonResponse(res, 200, result);
1062
1101
  } catch (e) {
1063
1102
  jsonResponse(res, 500, { error: e.message });
@@ -1093,10 +1132,11 @@ For allow rules, match the scope the user requests.
1093
1132
 
1094
1133
  IMPORTANT: Output ONLY the JSON object, no markdown fencing.
1095
1134
 
1096
- User request: "${query.replace(/"/g, '\\"')}"`;
1135
+ User request: (see below)`;
1097
1136
 
1098
1137
  try {
1099
- const result = await callClaude([{ role: 'user', content: prompt }], 256);
1138
+ // Pass query as a separate message to avoid prompt injection via string interpolation
1139
+ const result = await callClaude([{ role: 'user', content: prompt + '\n\n' + query }], 256);
1100
1140
  const text = result.content?.[0]?.text || '';
1101
1141
  const parsed = JSON.parse(text);
1102
1142
  return {
@@ -1155,12 +1195,17 @@ function handlePermAIException(res, query, parentRule, body) {
1155
1195
  },
1156
1196
  };
1157
1197
 
1158
- // Try to match known dangerous patterns
1198
+ // Try to match known dangerous patterns (longest match first to avoid duplicates)
1159
1199
  const binPatterns = dangerousPatterns[parentBin] || {};
1160
- for (const [key, denyRules] of Object.entries(binPatterns)) {
1200
+ const sortedKeys = Object.keys(binPatterns).sort((a, b) => b.length - a.length);
1201
+ let matched = false;
1202
+ for (const key of sortedKeys) {
1161
1203
  if (query.includes(key)) {
1162
- rules.push(...denyRules);
1163
- explanation = `Deny ${key} operations for ${parentBin}.`;
1204
+ for (const r of binPatterns[key]) {
1205
+ if (!rules.includes(r)) rules.push(r);
1206
+ }
1207
+ if (!matched) explanation = `Deny ${key} operations for ${parentBin}.`;
1208
+ matched = true;
1164
1209
  }
1165
1210
  }
1166
1211
 
@@ -1234,7 +1279,6 @@ function handleGetScanCache(req, res) {
1234
1279
 
1235
1280
  function handleScanToolUsage(req, res) {
1236
1281
  // Scan session JSONL files and extract tool usage, then generalize into rules
1237
- const { getAllSessionFiles } = require('./session-utils');
1238
1282
  const allFiles = getAllSessionFiles();
1239
1283
  const toolUsage = {}; // tool_name -> { count, commands: Map<binary, Set<subcmds>>, projects: Set }
1240
1284
 
@@ -1384,7 +1428,6 @@ function handleDeleteAutoApproval(req, res, id) {
1384
1428
  async function handleScanAutoApprovals(req, res) {
1385
1429
  // Scan session JSONL files and find approval patterns:
1386
1430
  // assistant asks a question -> user responds with short affirmative
1387
- const { getAllSessionFiles } = require('./session-utils');
1388
1431
  const allFiles = getAllSessionFiles();
1389
1432
  const recentFiles = allFiles
1390
1433
  .map(f => { try { return { ...f, mtime: fs.statSync(f.filePath).mtime }; } catch { return null; } })
@@ -1849,4 +1892,4 @@ function handleLifecycleRefresh(req, res) {
1849
1892
  } catch (e) { jsonResponse(res, 500, { error: e.message }); }
1850
1893
  }
1851
1894
 
1852
- module.exports = { handlePromptApi, queueEngine, importPermissionsToDb };
1895
+ module.exports = { handlePromptApi, queueEngine, importPermissionsToDb, runIncrementalConversationImport };