ninja-terminals 2.0.0 → 2.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.
@@ -0,0 +1,127 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const BACKEND_URL = process.env.NINJA_BACKEND_URL || 'https://emtchat-backend.onrender.com';
5
+ const SESSION_MARKER = '.ninja-prompt-session';
6
+
7
+ /**
8
+ * Fetch orchestrator prompt from backend based on user's subscription tier.
9
+ * @param {string} token - JWT auth token
10
+ * @param {string} projectDir - User's CWD where prompts will be written
11
+ * @returns {Promise<{promptLevel: string, filesWritten: string[]}>}
12
+ */
13
+ async function fetchAndWritePrompt(token, projectDir) {
14
+ const fetch = require('node-fetch');
15
+
16
+ const response = await fetch(`${BACKEND_URL}/api/ninja/orchestrator-prompt`, {
17
+ headers: {
18
+ 'Authorization': `Bearer ${token}`,
19
+ 'Content-Type': 'application/json'
20
+ }
21
+ });
22
+
23
+ if (!response.ok) {
24
+ const error = await response.text();
25
+ throw new Error(`Failed to fetch prompt: ${response.status} - ${error}`);
26
+ }
27
+
28
+ const data = await response.json();
29
+ const { promptLevel, prompt, workerRules, orchestratorFiles } = data;
30
+ const filesWritten = [];
31
+
32
+ // Free tier: no prompts delivered
33
+ if (promptLevel === 'none') {
34
+ return { promptLevel, filesWritten };
35
+ }
36
+
37
+ // Standard tier (lite): write ORCHESTRATOR-PROMPT.md only
38
+ if (promptLevel === 'lite' && prompt) {
39
+ const promptPath = path.join(projectDir, 'ORCHESTRATOR-PROMPT.md');
40
+ fs.writeFileSync(promptPath, prompt, 'utf8');
41
+ filesWritten.push('ORCHESTRATOR-PROMPT.md');
42
+ }
43
+
44
+ // Pro tier (full): write ORCHESTRATOR-PROMPT.md + orchestrator/ directory
45
+ if (promptLevel === 'full') {
46
+ if (prompt) {
47
+ const promptPath = path.join(projectDir, 'ORCHESTRATOR-PROMPT.md');
48
+ fs.writeFileSync(promptPath, prompt, 'utf8');
49
+ filesWritten.push('ORCHESTRATOR-PROMPT.md');
50
+ }
51
+
52
+ if (orchestratorFiles && typeof orchestratorFiles === 'object') {
53
+ const orchestratorDir = path.join(projectDir, 'orchestrator');
54
+ if (!fs.existsSync(orchestratorDir)) {
55
+ fs.mkdirSync(orchestratorDir, { recursive: true });
56
+ }
57
+
58
+ for (const [filename, content] of Object.entries(orchestratorFiles)) {
59
+ const filePath = path.join(orchestratorDir, filename);
60
+ fs.writeFileSync(filePath, content, 'utf8');
61
+ filesWritten.push(`orchestrator/${filename}`);
62
+ }
63
+ }
64
+ }
65
+
66
+ // Write session marker so we know these prompts were delivered (not user-owned)
67
+ if (filesWritten.length > 0) {
68
+ const markerPath = path.join(projectDir, SESSION_MARKER);
69
+ const markerData = {
70
+ deliveredAt: new Date().toISOString(),
71
+ promptLevel,
72
+ files: filesWritten
73
+ };
74
+ fs.writeFileSync(markerPath, JSON.stringify(markerData, null, 2), 'utf8');
75
+ }
76
+
77
+ return { promptLevel, filesWritten };
78
+ }
79
+
80
+ /**
81
+ * Clean up delivered prompts on session end / logout.
82
+ * Only deletes files that we delivered (tracked via session marker).
83
+ * @param {string} projectDir - User's CWD
84
+ * @returns {Promise<{cleaned: boolean, filesRemoved: string[]}>}
85
+ */
86
+ async function cleanupPrompts(projectDir) {
87
+ const markerPath = path.join(projectDir, SESSION_MARKER);
88
+ const filesRemoved = [];
89
+
90
+ // Check if we delivered prompts in this session
91
+ if (!fs.existsSync(markerPath)) {
92
+ return { cleaned: false, filesRemoved };
93
+ }
94
+
95
+ try {
96
+ const markerData = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
97
+ const deliveredFiles = markerData.files || [];
98
+
99
+ // Remove each delivered file
100
+ for (const file of deliveredFiles) {
101
+ const filePath = path.join(projectDir, file);
102
+ if (fs.existsSync(filePath)) {
103
+ fs.unlinkSync(filePath);
104
+ filesRemoved.push(file);
105
+ }
106
+ }
107
+
108
+ // Remove orchestrator directory if it's now empty
109
+ const orchestratorDir = path.join(projectDir, 'orchestrator');
110
+ if (fs.existsSync(orchestratorDir)) {
111
+ const remaining = fs.readdirSync(orchestratorDir);
112
+ if (remaining.length === 0) {
113
+ fs.rmdirSync(orchestratorDir);
114
+ }
115
+ }
116
+
117
+ // Remove the session marker
118
+ fs.unlinkSync(markerPath);
119
+
120
+ return { cleaned: true, filesRemoved };
121
+ } catch (err) {
122
+ console.error('Error cleaning up prompts:', err.message);
123
+ return { cleaned: false, filesRemoved };
124
+ }
125
+ }
126
+
127
+ module.exports = { fetchAndWritePrompt, cleanupPrompts };
@@ -7,6 +7,45 @@ const path = require('path');
7
7
  // Worker settings generator
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
+ // ---------------------------------------------------------------------------
11
+ // Tier-based permission definitions
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const TIER_PERMISSIONS = {
15
+ // Free tier: basic file operations only
16
+ free: {
17
+ tools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'],
18
+ mcp: [], // No MCP tools
19
+ network: false,
20
+ agents: false,
21
+ },
22
+ // Standard tier: + network tools
23
+ standard: {
24
+ tools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'],
25
+ mcp: [], // No MCP tools
26
+ network: true, // WebFetch, WebSearch
27
+ agents: true,
28
+ },
29
+ // Pro tier: full access including all MCP tools
30
+ pro: {
31
+ tools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'],
32
+ mcp: [
33
+ 'mcp__studychat__*',
34
+ 'mcp__postforme__*',
35
+ 'mcp__render-billing__*',
36
+ 'mcp__netlify-billing__*',
37
+ 'mcp__chrome-devtools__*',
38
+ 'mcp__gkchatty-production__*',
39
+ 'mcp__builder-pro-mcp__*',
40
+ 'mcp__gmail__*',
41
+ 'mcp__c2c__*',
42
+ 'mcp__atlas-architect__*',
43
+ ],
44
+ network: true,
45
+ agents: true,
46
+ },
47
+ };
48
+
10
49
  /**
11
50
  * Generate a Claude Code worker settings object for a terminal.
12
51
  *
@@ -14,15 +53,20 @@ const path = require('path');
14
53
  * @param {string|string[]} scope - File scope path(s), or '*'/'' for unrestricted
15
54
  * @param {Object} [options={}]
16
55
  * @param {number} [options.port=3000] - Server port for hook URLs
56
+ * @param {string} [options.tier='pro'] - User tier: 'free', 'standard', 'pro'
17
57
  * @param {string[]} [options.additionalAllow=[]] - Extra allow rules to merge
18
58
  * @param {string[]} [options.additionalDeny=[]] - Extra deny rules to merge
19
59
  * @returns {Object} Settings object suitable for `.claude/settings.local.json`
20
60
  */
21
61
  function generateWorkerSettings(terminalId, scope, options = {}) {
22
62
  const port = options.port || 3000;
63
+ const tier = options.tier || 'pro';
23
64
  const additionalAllow = options.additionalAllow || [];
24
65
  const additionalDeny = options.additionalDeny || [];
25
66
 
67
+ // Get tier permissions (default to free if unknown tier)
68
+ const tierPerms = TIER_PERMISSIONS[tier] || TIER_PERMISSIONS.free;
69
+
26
70
  // Build Edit/Write rules based on scope
27
71
  const editWriteRules = [];
28
72
  const unrestricted = !scope || scope === '*' || (Array.isArray(scope) && scope.length === 0);
@@ -39,12 +83,13 @@ function generateWorkerSettings(terminalId, scope, options = {}) {
39
83
  }
40
84
  }
41
85
 
86
+ // Base permissions for all tiers
42
87
  const allow = [
43
88
  'Read',
44
89
  'Glob',
45
90
  'Grep',
46
91
  ...editWriteRules,
47
- // Safe bash commands
92
+ // Safe bash commands (all tiers)
48
93
  'Bash(npm test *)',
49
94
  'Bash(npm run *)',
50
95
  'Bash(node *)',
@@ -59,22 +104,23 @@ function generateWorkerSettings(terminalId, scope, options = {}) {
59
104
  'Bash(tail *)',
60
105
  'Bash(mkdir *)',
61
106
  'Bash(cp *)',
62
- // MCP tools — all enabled servers
63
- 'mcp__studychat__*',
64
- 'mcp__postforme__*',
65
- 'mcp__render-billing__*',
66
- 'mcp__netlify-billing__*',
67
- 'mcp__chrome-devtools__*',
68
- 'mcp__gkchatty-production__*',
69
- 'mcp__builder-pro-mcp__*',
70
- 'mcp__gmail__*',
71
- 'mcp__c2c__*',
72
- 'mcp__atlas-architect__*',
73
- // Network and research
74
- 'WebFetch(*)',
75
- 'WebSearch(*)',
76
- // Additional bash
77
- 'Bash(curl *)',
107
+ ];
108
+
109
+ // Add tier-specific permissions
110
+ if (tierPerms.network) {
111
+ allow.push('WebFetch(*)', 'WebSearch(*)');
112
+ allow.push('Bash(curl *)');
113
+ }
114
+
115
+ if (tierPerms.agents) {
116
+ allow.push('Agent(*)');
117
+ }
118
+
119
+ // Add MCP tools for the tier
120
+ allow.push(...tierPerms.mcp);
121
+
122
+ // Additional bash commands (all tiers)
123
+ allow.push(
78
124
  'Bash(cd *)',
79
125
  'Bash(grep *)',
80
126
  'Bash(find *)',
@@ -86,10 +132,10 @@ function generateWorkerSettings(terminalId, scope, options = {}) {
86
132
  'Bash(git add *)',
87
133
  'Bash(git commit *)',
88
134
  'Bash(git push *)',
89
- // Sub-agents
90
- 'Agent(*)',
91
- ...additionalAllow,
92
- ];
135
+ );
136
+
137
+ // Merge additional allows
138
+ allow.push(...additionalAllow);
93
139
 
94
140
  const deny = [
95
141
  'Bash(rm -rf *)',
@@ -121,6 +167,7 @@ function generateWorkerSettings(terminalId, scope, options = {}) {
121
167
  * @param {string} projectDir - Absolute path to the project directory
122
168
  * @param {string|string[]} scope - File scope path(s)
123
169
  * @param {Object} [options={}]
170
+ * @param {string} [options.tier='pro'] - User tier for permission gating
124
171
  * @returns {string} Absolute path to the written settings file
125
172
  */
126
173
  function writeWorkerSettings(terminalId, projectDir, scope, options = {}) {
@@ -145,15 +192,27 @@ function writeWorkerSettings(terminalId, projectDir, scope, options = {}) {
145
192
  const mergedAllow = [...new Set([...(existing.permissions?.allow || []), ...settings.permissions.allow])];
146
193
  const mergedDeny = [...new Set([...(existing.permissions?.deny || []), ...settings.permissions.deny])];
147
194
 
195
+ // Build hooks for self-improvement loop (matches Claude Code's nested format)
196
+ const ninjaDir = path.resolve(__dirname, '..');
197
+ const hooks = {
198
+ PostToolUse: [{
199
+ matcher: '',
200
+ hooks: [{
201
+ type: 'command',
202
+ command: path.join(ninjaDir, '.claude/hooks/track-tool.sh'),
203
+ }],
204
+ }],
205
+ };
206
+
148
207
  const merged = {
149
208
  ...existing,
150
209
  permissions: { allow: mergedAllow, deny: mergedDeny },
210
+ hooks,
151
211
  sandbox: settings.sandbox,
152
- // Preserve existing hooks, enabledMcpjsonServers, etc.
153
212
  };
154
213
 
155
214
  fs.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
156
215
  return settingsPath;
157
216
  }
158
217
 
159
- module.exports = { generateWorkerSettings, writeWorkerSettings };
218
+ module.exports = { generateWorkerSettings, writeWorkerSettings, TIER_PERMISSIONS };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "ninja-terminals",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Multi-terminal Claude Code orchestrator with DAG task management, permission hooks, and resilience",
5
5
  "main": "server.js",
6
6
  "bin": {
7
- "ninja-terminals": "./cli.js"
7
+ "ninja-terminals": "cli.js"
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node server.js"
@@ -12,11 +12,9 @@
12
12
  "files": [
13
13
  "lib/",
14
14
  "public/",
15
- "orchestrator/",
16
15
  "cli.js",
17
16
  "server.js",
18
- "CLAUDE.md",
19
- "ORCHESTRATOR-PROMPT.md"
17
+ "CLAUDE.md"
20
18
  ],
21
19
  "keywords": [
22
20
  "claude",
@@ -31,7 +29,7 @@
31
29
  "license": "MIT",
32
30
  "repository": {
33
31
  "type": "git",
34
- "url": "https://github.com/davidmorin/ninja-terminals"
32
+ "url": "git+https://github.com/dmos82/ninja-terminals.git"
35
33
  },
36
34
  "homepage": "https://ninjaterminals.com",
37
35
  "engines": {
@@ -39,7 +37,11 @@
39
37
  },
40
38
  "type": "commonjs",
41
39
  "dependencies": {
40
+ "@anthropic-ai/sdk": "^0.80.0",
41
+ "cheerio": "^1.2.0",
42
42
  "express": "^5.2.1",
43
+ "multer": "^2.1.1",
44
+ "node-fetch": "^2.7.0",
43
45
  "node-pty": "^1.2.0-beta.10",
44
46
  "ws": "^8.19.0"
45
47
  }