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.
- package/CLAUDE.md +2 -17
- package/cli.js +23 -0
- package/lib/auth.js +195 -0
- package/lib/hypothesis-validator.js +346 -0
- package/lib/post-session.js +426 -0
- package/lib/pre-dispatch.js +265 -0
- package/lib/prompt-delivery.js +127 -0
- package/lib/settings-gen.js +82 -23
- package/package.json +8 -6
- package/public/app.js +282 -13
- package/public/index.html +45 -0
- package/public/style.css +300 -0
- package/server.js +358 -33
- package/ORCHESTRATOR-PROMPT.md +0 -295
- package/orchestrator/evolution-log.md +0 -33
- package/orchestrator/identity.md +0 -60
- package/orchestrator/metrics/.gitkeep +0 -0
- package/orchestrator/metrics/raw/.gitkeep +0 -0
- package/orchestrator/metrics/session-2026-03-23-setup.md +0 -54
- package/orchestrator/metrics/session-2026-03-24-appcast-build.md +0 -55
- package/orchestrator/playbooks.md +0 -71
- package/orchestrator/security-protocol.md +0 -69
- package/orchestrator/tool-registry.md +0 -96
|
@@ -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 };
|
package/lib/settings-gen.js
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
'
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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.
|
|
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": "
|
|
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/
|
|
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
|
}
|